endpoint setup

This commit is contained in:
exolonConfidental
2026-02-08 11:18:47 +05:30
parent 8fb3b7cf67
commit a77788fc47
33 changed files with 932 additions and 352 deletions

View File

@@ -0,0 +1,40 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_async_session
from app.services.location_service import LocationService
from app.schemas.map_schema import MapQueryResponse
router = APIRouter(prefix="/map", tags=["Map"])
@router.get("/query", response_model=MapQueryResponse)
async def query_nearby_locations(
lat: float = Query(..., ge=-90, le=90),
lon: float = Query(..., ge=-180, le=180),
radius: int = Query(2000, gt=0, le=50000),
session: AsyncSession = Depends(get_async_session),
):
"""
Returns all locations within a given radius from a lat/lon point.
Uses PostGIS ST_DWithin (geography).
"""
try:
locations = await LocationService.get_nearby_locations(
session=session,
lat=lat,
lon=lon,
radius= radius
)
return MapQueryResponse(
total=len(locations),
locations=locations
)
except Exception as e:
raise HTTPException(
status_code=500,
detail="Failed to fetch nearby locations"
)

View File

@@ -0,0 +1,93 @@
from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_async_session
from app.services.property_service import PropertyService
from app.schemas.property_schema import PropertyResponse
from app.schemas.full_entry_schema import FullEntryCreateRequest
from app.schemas.property_update_request import PropertyOwnerUpdateRequest
router = APIRouter(prefix="/property", tags=["Property"])
@router.get("/by-location/{location_id}", response_model=PropertyResponse)
async def get_property_by_location(
location_id: int,
session: AsyncSession = Depends(get_async_session),
):
"""
Fetch property + owner using location_id.
Called when agent clicks map box.
"""
property_obj = await PropertyService.get_property_by_location_id(
session=session,
location_id=location_id
)
if not property_obj:
raise HTTPException(
status_code=404,
detail="Property not found for given location"
)
return property_obj
@router.post("/create-full", response_model=PropertyResponse)
async def create_full_entry(
payload: FullEntryCreateRequest,
session: AsyncSession = Depends(get_async_session),
):
"""
Creates Location + Owner + Property in a single transaction.
Frontend cannot send IDs.
"""
try:
property_obj = await PropertyService.create_full_entry(
session=session,
location_data=payload.location,
owner_data=payload.owner,
property_data=payload.property,
)
return property_obj
except Exception as e:
raise HTTPException(
status_code=500,
detail="Failed to create property entry"
)
@router.put("/{property_id}", response_model=PropertyResponse)
async def update_property_and_owner(
property_id: int = Path(..., gt=0),
payload: PropertyOwnerUpdateRequest = ...,
session: AsyncSession = Depends(get_async_session),
):
"""
Update Property + Owner details.
Partial updates supported.
"""
if not payload.owner and not payload.property:
raise HTTPException(
status_code=400,
detail="No update data provided"
)
property_obj = await PropertyService.update_property_and_owner(
session=session,
property_id=property_id,
owner_data=payload.owner.dict(exclude_unset=True) if payload.owner else None,
property_data=payload.property.dict(exclude_unset=True) if payload.property else None,
)
if not property_obj:
raise HTTPException(
status_code=404,
detail="Property not found"
)
return property_obj

View File

@@ -1,35 +0,0 @@
import logging
from fastapi import APIRouter, HTTPException
from app.core.neo4j import get_driver
from app.services.property_service import get_nearest_asset
from app.schemas.location_request import NearestAssetRequest
from app.schemas.property_owner_response import NearestAssetResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/properties", tags=["Geo Search"])
@router.post("/nearest", response_model=NearestAssetResponse)
def nearest_property(payload: NearestAssetRequest):
logger.info(f"Incoming geo request lat={payload.lat}, lng={payload.lng}")
try:
driver = get_driver()
result = get_nearest_asset(driver, payload.lat, payload.lng)
logger.info("Nearest asset query successful")
return result
except ValueError as e:
logger.warning(f"No asset found: {str(e)}")
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.exception("Unexpected server error")
raise HTTPException(
status_code=500,
detail="Internal Server Error"
)

View File

@@ -1,5 +1,7 @@
from fastapi import APIRouter
from app.api.v1.property_routes import router as property_routes
from app.api.v1.endpoints.map import router as map_routes
from app.api.v1.endpoints.property import router as property_routes
api_router = APIRouter()
api_router.include_router(map_routes)
api_router.include_router(property_routes)

View File

@@ -1,12 +0,0 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
NEO4J_URI: str
NEO4J_USER: str
NEO4J_PASSWORD: str
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -1,10 +0,0 @@
from neo4j import GraphDatabase
from app.core.config import settings
driver = GraphDatabase.driver(
settings.NEO4J_URI,
auth=(settings.NEO4J_USER, settings.NEO4J_PASSWORD)
)
def get_driver():
return driver

View File

@@ -0,0 +1,83 @@
from datetime import date
from sqlalchemy import (
String,
Integer,
Boolean,
Date,
ForeignKey,
Text
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.models.base import Base
class InsuranceDetails(Base):
__tablename__ = "insurance_details"
id: Mapped[int] = mapped_column(primary_key=True)
# 🔗 Relation with Property
property_id: Mapped[int] = mapped_column(
ForeignKey("properties.id", ondelete="CASCADE"),
index=True
)
# -----------------------------
# REQUIRED FIELDS (Your spec)
# -----------------------------
insurance_company: Mapped[str] = mapped_column(String(150))
claim_number: Mapped[str] = mapped_column(String(100), unique=True)
date_of_loss: Mapped[date] = mapped_column(Date)
adjuster_name: Mapped[str] = mapped_column(String(150), nullable=True)
adjuster_phone: Mapped[str] = mapped_column(String(30), nullable=True)
adjuster_email: Mapped[str] = mapped_column(String(150), nullable=True)
claim_filed: Mapped[bool] = mapped_column(Boolean, default=False)
claim_approved: Mapped[bool] = mapped_column(Boolean, default=False)
# -----------------------------
# 🇺🇸 US Insurance Standard Fields
# -----------------------------
policy_number: Mapped[str] = mapped_column(String(100), nullable=True)
coverage_type: Mapped[str] = mapped_column(
String(50), nullable=True
)
# e.g. Homeowners, Flood, Fire, Liability, Windstorm
claim_type: Mapped[str] = mapped_column(
String(50), nullable=True
)
# e.g. Roof Damage, Water Damage, Fire, Storm, Theft
deductible_amount: Mapped[int] = mapped_column(nullable=True)
claim_amount: Mapped[int] = mapped_column(nullable=True)
approved_amount: Mapped[int] = mapped_column(nullable=True)
payment_status: Mapped[str] = mapped_column(
String(50), nullable=True
)
# Pending / Paid / Denied / Under Review
date_claim_filed: Mapped[date] = mapped_column(Date, nullable=True)
date_claim_closed: Mapped[date] = mapped_column(Date, nullable=True)
insurance_agent_name: Mapped[str] = mapped_column(String(150), nullable=True)
insurance_agent_phone: Mapped[str] = mapped_column(String(30), nullable=True)
notes: Mapped[str] = mapped_column(Text, nullable=True)
# -----------------------------
# Relationship
# -----------------------------
property = relationship("Property", back_populates="insurance_records")

View File

@@ -1,4 +1,5 @@
from sqlalchemy import String, Integer, Float, BigInteger
from geoalchemy2 import Geography
from sqlalchemy import String, Integer, Float, BigInteger, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column
from app.db.models.base import Base
@@ -7,6 +8,9 @@ from app.db.models.base import Base
class Location(Base):
__tablename__ = "locations"
__table_args__ = (
UniqueConstraint("osm_type", "osm_id", name="uq_osm_location"),
)
id: Mapped[int] = mapped_column(primary_key=True, index=True)
@@ -38,6 +42,10 @@ class Location(Base):
bbox_lon_min: Mapped[float] = mapped_column(Float)
bbox_lon_max: Mapped[float] = mapped_column(Float)
geom: Mapped[str] = mapped_column(
Geography("POINT", srid=4326)
)
property = relationship(
"Property",
back_populates="location",

View File

@@ -63,3 +63,9 @@ class Property(Base):
back_populates="property",
uselist=False,
)
insurance_records = relationship(
"InsuranceDetails",
back_populates="property",
cascade="all, delete-orphan"
)

View File

@@ -8,6 +8,6 @@ AsyncSessionLocal = async_sessionmaker(
)
async def get_db():
async def get_async_session():
async with AsyncSessionLocal() as session:
yield session

View File

@@ -0,0 +1,50 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models.location import Location
from geoalchemy2.shape import from_shape
from shapely.geometry import Point
from geoalchemy2.functions import ST_DWithin
class LocationRepository:
@staticmethod
async def get_by_osm(session: AsyncSession, osm_type: str, osm_id: int):
stmt = select(Location).where(
Location.osm_type == osm_type,
Location.osm_id == osm_id
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def create(session, data):
# For geography this is still correct
geom = from_shape(Point(data["longitude"], data["latitude"]), srid=4326)
location = Location(
**data,
geom=geom
)
session.add(location)
await session.flush()
return location
@staticmethod
async def find_within_radius(session, lat, lon, radius_m=2000):
# Create a geography point instead of string
point = from_shape(Point(lon, lat), srid=4326)
stmt = select(Location).where(
ST_DWithin(
Location.geom,
point,
radius_m
)
)
result = await session.execute(stmt)
return result.scalars().all()

View File

@@ -0,0 +1,8 @@
class OwnerRepository:
@staticmethod
async def update(session, owner, data: dict):
for key, value in data.items():
setattr(owner, key, value)
await session.flush()
return owner

View File

@@ -1,110 +1,20 @@
from neo4j import Driver
QUERY_NEAREST = """
WITH point({latitude: $lat, longitude: $lng}) AS userLocation
MATCH (o:Owner)-[:OWNS]->(p:Property)-[:LOCATED_AT]->(l:Location)
WHERE l.location IS NOT NULL
WITH o, p,
point.distance(l.location, userLocation) AS distanceMeters
ORDER BY distanceMeters ASC
LIMIT 1
RETURN
// Owner Projection (API Safe)
{
owner_id: o.owner_id,
full_name: o.full_name,
email: o.email,
phone: o.phone,
entity_type: o.entity_type,
mailing_address: o.mailing_address,
credit_score_est: o.credit_score_est,
income_bracket: o.income_bracket,
net_worth_est: o.net_worth_est,
portfolio_size: o.portfolio_size,
min_price_expectation: o.min_price_expectation,
preferred_close_days: o.preferred_close_days,
urgency_score: o.urgency_score,
is_absentee: o.is_absentee,
willing_to_sell: o.willing_to_sell,
willing_to_partner: o.willing_to_partner
} AS owner,
// Property Projection (API Safe)
{
property_id: p.property_id,
internal_asset_code: p.internal_asset_code,
structure_type: p.structure_type,
listing_status: p.listing_status,
occupancy_status: p.occupancy_status,
property_grade: p.property_grade,
energy_rating: p.energy_rating,
year_built: p.year_built,
floors_count: p.floors_count,
bedrooms: p.bedrooms,
bathrooms: p.bathrooms,
total_built_sqft: p.total_built_sqft,
lot_size_sqft: p.lot_size_sqft,
garage_spaces: p.garage_spaces,
purchase_price: p.purchase_price,
expected_sale_price: p.expected_sale_price,
market_value_est: p.market_value_est,
current_rent: p.current_rent,
rental_yield_percent: p.rental_yield_percent,
vacancy_days: p.vacancy_days,
tenant_present: p.tenant_present,
exterior_condition: p.exterior_condition,
foundation_type: p.foundation_type,
roof_type: p.roof_type,
roof_material: p.roof_material,
roof_condition: p.roof_condition,
roof_pitch: p.roof_pitch,
roof_age_years: p.roof_age_years,
siding_material: p.siding_material,
gutter_status: p.gutter_status,
hvac_type: p.hvac_type,
electric_type: p.electric_type,
plumbing_type: p.plumbing_type,
solar_installed: p.solar_installed,
mold_risk_level: p.mold_risk_level,
termite_risk_level: p.termite_risk_level,
structural_risk_level: p.structural_risk_level,
fire_damage_flag: p.fire_damage_flag,
water_damage_flag: p.water_damage_flag,
created_at: toString(p.created_at),
updated_at: toString(p.updated_at)
} AS property,
distanceMeters
"""
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.db.models.property import Property
def find_nearest_property_and_owner(driver: Driver, lat: float, lng: float):
class PropertyRepository:
@staticmethod
async def get_by_location_id(session, location_id: int):
with driver.session(database="neo4j") as session:
result = session.run(
QUERY_NEAREST,
lat=lat,
lng=lng
stmt = (
select(Property)
.options(
selectinload(Property.owner),
selectinload(Property.location),
)
.where(Property.location_id == location_id)
)
record = result.single()
if not record:
return None
return {
"owner": record["owner"],
"property": record["property"],
"distanceMeters": record["distanceMeters"]
}
result = await session.execute(stmt)
return result.scalar_one_or_none()

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel
from app.schemas.location_create_schema import LocationCreate
from app.schemas.owner_create_schema import OwnerCreate
from app.schemas.property_create_schema import PropertyCreate
class FullEntryCreateRequest(BaseModel):
location: LocationCreate
owner: OwnerCreate
property: PropertyCreate

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
class LocationCreate(BaseModel):
place_id: int
osm_id: int
osm_type: str
latitude: float
longitude: float
house_number: str | None = None
road: str
city: str
county: str
state: str
postcode: str
country: str
country_code: str
display_name: str
bbox_lat_min: float
bbox_lat_max: float
bbox_lon_min: float
bbox_lon_max: float

View File

@@ -1,6 +0,0 @@
from pydantic import BaseModel, Field
class NearestAssetRequest(BaseModel):
lat: float = Field(..., ge=-90, le=90, example=34.01544)
lng: float = Field(..., ge=-180, le=180, example=-118.2201)

49
app/schemas/map_schema.py Normal file
View File

@@ -0,0 +1,49 @@
from pydantic import BaseModel, Field
class MapQueryParams(BaseModel):
lat: float = Field(..., ge=-90, le=90, description="Latitude")
lon: float = Field(..., ge=-180, le=180, description="Longitude")
radius: int = Field(2000, gt=0, le=50000, description="Search radius in meters")
class LocationResponse(BaseModel):
id: int
# OSM identifiers
place_id: int
osm_id: int
osm_type: str
# Coordinates
latitude: float
longitude: float
# Address info
house_number: str | None = None
road: str
city: str
county: str
state: str
postcode: str
country: str
country_code: str
# Display name
display_name: str
# Bounding box
bbox_lat_min: float
bbox_lat_max: float
bbox_lon_min: float
bbox_lon_max: float
class Config:
from_attributes = True
class MapQueryResponse(BaseModel):
total: int
locations: list[LocationResponse]

View File

@@ -0,0 +1,15 @@
from pydantic import BaseModel
class OwnerCreate(BaseModel):
full_name: str
phone_number: str
email: str
occupation: str
annual_income_range: str
willing_to_rent: bool = False
desired_rent_price: int | None = None
willing_to_sell: bool = False
desired_sell_price: int | None = None

View File

@@ -0,0 +1,17 @@
from pydantic import BaseModel
class OwnerResponse(BaseModel):
id: int
full_name: str
phone_number: str
email: str
occupation: str
annual_income_range: str
willing_to_rent: bool
desired_rent_price: int | None
willing_to_sell: bool
desired_sell_price: int | None
class Config:
from_attributes = True

View File

@@ -0,0 +1,15 @@
from pydantic import BaseModel
class OwnerUpdate(BaseModel):
full_name: str | None = None
phone_number: str | None = None
email: str | None = None
occupation: str | None = None
annual_income_range: str | None = None
willing_to_rent: bool | None = None
desired_rent_price: int | None = None
willing_to_sell: bool | None = None
desired_sell_price: int | None = None

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
from datetime import date
class PropertyCreate(BaseModel):
property_type: str
built_up_area: float
bedrooms: int
bathrooms: int
purchase_date: date
purchase_price: float
sale_listing_date: date | None = None
sale_asking_price: float | None = None
last_renovation_date: date | None = None
renovation_description: str | None = None
roof_repair_date: date | None = None
roof_condition: str
available_for_rent: bool = False
expected_rent: float | None = None
rental_available_date: date | None = None

View File

@@ -1,172 +0,0 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# -----------------------------
# Owner Response Schema
# -----------------------------
class OwnerResponse(BaseModel):
owner_id: str = Field(..., example="OWN_9001")
full_name: str = Field(..., example="Michael Anderson")
email: Optional[str] = Field(None, example="michael.anderson@email.com")
phone: Optional[str] = Field(None, example="+14155552671")
entity_type: str = Field(..., example="Individual")
mailing_address: Optional[str] = Field(
None,
example="742 Evergreen Terrace, San Diego, CA 92101"
)
credit_score_est: Optional[int] = Field(None, ge=300, le=850, example=780)
income_bracket: Optional[str] = Field(None, example="$100k+")
net_worth_est: Optional[int] = Field(None, example=1250000)
portfolio_size: Optional[int] = Field(None, example=3)
min_price_expectation: Optional[int] = Field(None, example=650000)
preferred_close_days: Optional[int] = Field(None, example=30)
urgency_score: Optional[int] = Field(None, ge=0, le=100, example=72)
is_absentee: Optional[bool] = Field(None, example=True)
willing_to_sell: Optional[bool] = Field(None, example=True)
willing_to_partner: Optional[bool] = Field(None, example=False)
# ⚠ Recommended: Do not expose in public APIs
tax_id_hash: Optional[str] = Field(
None,
example="HASHX9921",
description="Hashed tax identifier (masked)"
)
class Config:
from_attributes = True
# -----------------------------
# Property Response Schema
# -----------------------------
class PropertyResponse(BaseModel):
property_id: str = Field(..., example="PROP_1001")
internal_asset_code: str = Field(..., example="ASSET-LA-7782")
structure_type: str = Field(..., example="SingleFamily")
listing_status: str = Field(..., example="ForSale")
occupancy_status: str = Field(..., example="OwnerOccupied")
property_grade: str = Field(..., example="A")
energy_rating: Optional[str] = Field(None, example="B+")
year_built: int = Field(..., example=2006)
floors_count: int = Field(..., example=2)
bedrooms: int = Field(..., example=4)
bathrooms: int = Field(..., example=3)
total_built_sqft: int = Field(..., example=2480)
lot_size_sqft: Optional[int] = Field(None, example=6200)
garage_spaces: Optional[int] = Field(None, example=2)
purchase_price: Optional[int] = Field(None, example=590000)
expected_sale_price: Optional[int] = Field(None, example=740000)
market_value_est: Optional[int] = Field(None, example=685000)
current_rent: Optional[int] = Field(None, example=0)
rental_yield_percent: Optional[float] = Field(None, example=6.2)
vacancy_days: Optional[int] = Field(None, example=0)
tenant_present: Optional[bool] = Field(None, example=False)
# Structural / Condition
exterior_condition: Optional[str] = Field(None, example="Good")
foundation_type: Optional[str] = Field(None, example="Slab")
roof_type: Optional[str] = Field(None, example="Gable")
roof_material: Optional[str] = Field(None, example="Asphalt Shingle")
roof_condition: Optional[str] = Field(None, example="Good")
roof_pitch: Optional[str] = Field(None, example="Medium")
roof_age_years: Optional[int] = Field(None, example=7)
siding_material: Optional[str] = Field(None, example="Hardie Board")
gutter_status: Optional[str] = Field(None, example="Functional")
hvac_type: Optional[str] = Field(None, example="Central")
electric_type: Optional[str] = Field(None, example="Copper")
plumbing_type: Optional[str] = Field(None, example="PEX")
solar_installed: Optional[bool] = Field(None, example=False)
# Risk Indicators
mold_risk_level: Optional[str] = Field(None, example="Low")
termite_risk_level: Optional[str] = Field(None, example="Low")
structural_risk_level: Optional[str] = Field(None, example="Low")
fire_damage_flag: Optional[bool] = Field(None, example=False)
water_damage_flag: Optional[bool] = Field(None, example=False)
# Timestamps
created_at: Optional[datetime] = Field(
None,
example="2026-01-29T16:43:44.285Z"
)
updated_at: Optional[datetime] = Field(
None,
example="2026-01-29T16:43:44.285Z"
)
class Config:
from_attributes = True
# -----------------------------
# Final Nearest Asset Response
# -----------------------------
class NearestAssetResponse(BaseModel):
owner: OwnerResponse
property: PropertyResponse
distanceMeters: float = Field(..., example=342.55)

View File

@@ -0,0 +1,33 @@
from pydantic import BaseModel
from datetime import date
from app.schemas.owner_schema import OwnerResponse
class PropertyResponse(BaseModel):
id: int
property_type: str
built_up_area: float
bedrooms: int
bathrooms: int
purchase_date: date
purchase_price: float
sale_listing_date: date | None
sale_asking_price: float | None
last_renovation_date: date | None
renovation_description: str | None
roof_repair_date: date | None
roof_condition: str
available_for_rent: bool
expected_rent: float | None
rental_available_date: date | None
owner: OwnerResponse
class Config:
from_attributes = True

View File

@@ -0,0 +1,8 @@
from pydantic import BaseModel
from app.schemas.owner_update_schema import OwnerUpdate
from app.schemas.property_update_schema import PropertyUpdate
class PropertyOwnerUpdateRequest(BaseModel):
owner: OwnerUpdate | None = None
property: PropertyUpdate | None = None

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
from datetime import date
class PropertyUpdate(BaseModel):
property_type: str | None = None
built_up_area: float | None = None
bedrooms: int | None = None
bathrooms: int | None = None
purchase_date: date | None = None
purchase_price: float | None = None
sale_listing_date: date | None = None
sale_asking_price: float | None = None
last_renovation_date: date | None = None
renovation_description: str | None = None
roof_repair_date: date | None = None
roof_condition: str | None = None
available_for_rent: bool | None = None
expected_rent: float | None = None
rental_available_date: date | None = None

View File

@@ -0,0 +1,14 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.location_repo import LocationRepository
class LocationService:
@staticmethod
async def get_nearby_locations(session: AsyncSession, lat: float, lon: float, radius: int = 2000):
return await LocationRepository.find_within_radius(
session=session,
lat=lat,
lon=lon,
radius_m=radius
)

View File

@@ -1,10 +1,102 @@
from app.repositories.property_repo import find_nearest_property_and_owner
from app.repositories.location_repo import LocationRepository
from app.repositories.property_repo import PropertyRepository
from app.db.models.owner import Owner
from app.db.models.property import Property
def get_nearest_asset(driver, lat: float, lng: float):
data = find_nearest_property_and_owner(driver, lat, lng)
class PropertyService:
@staticmethod
async def get_property_by_osm(session, osm_type, osm_id):
location = await LocationRepository.get_by_osm(
session, osm_type, osm_id
)
if not location:
return None
property = await PropertyRepository.get_by_location_id(
session, location.id
)
return property
@staticmethod
async def create_full_entry(session, location_data, owner_data, property_data):
async with session.begin():
# Check existing location
location = await LocationRepository.get_by_osm(
session,
location_data.osm_type,
location_data.osm_id
)
if not location:
location = await LocationRepository.create(
session,
location_data.dict()
)
# Check if property already exists for this location
existing_property = await PropertyRepository.get_by_location_id(
session,
location.id
)
if existing_property:
# return existing instead of crashing
return existing_property
# Create owner
owner = Owner(**owner_data.dict())
session.add(owner)
await session.flush()
# Create property
prop_data = property_data.dict()
prop_data["owner_id"] = owner.id
prop_data["location_id"] = location.id
property_obj = Property(**prop_data)
session.add(property_obj)
return property_obj
@staticmethod
async def update_property_and_owner(session, property_id, owner_data, property_data):
async with session.begin():
property_obj = await session.get(Property, property_id)
if not property_obj:
return None
owner = property_obj.owner
# Update owner
if owner_data:
for k, v in owner_data.items():
setattr(owner, k, v)
# Update property
if property_data:
for k, v in property_data.items():
setattr(property_obj, k, v)
return property_obj
@staticmethod
async def get_property_by_location_id(session, location_id: int):
property_obj = await PropertyRepository.get_by_location_id(
session, location_id
)
return property_obj
if not data:
raise ValueError("No nearby property found")
return data