diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 734d1c9..5e051d0 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -42,7 +42,12 @@ class Settings(BaseSettings): GOOGLE_ADS_CLIENT_ID: Optional[str] = None GOOGLE_ADS_CLIENT_SECRET: Optional[str] = None GOOGLE_ADS_DEVELOPER_TOKEN: Optional[str] = None - GOOGLE_ADS_REDIRECT_URI: str = "http://localhost:3000/settings/integrations/google-ads/callback" + GOOGLE_ADS_REDIRECT_URI: str = ( + "http://localhost:3000/settings/integrations/google-ads/callback" + ) + GOOGLE_ADS_SYNC_DELAY_SECONDS: int = ( + 5 # Delay before fetching user list stats after upload + ) # Pagination DEFAULT_PAGE_SIZE: int = 100 diff --git a/backend/app/schemas/property.py b/backend/app/schemas/property.py index 2ff31a5..d67c8b8 100644 --- a/backend/app/schemas/property.py +++ b/backend/app/schemas/property.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, field_validator, ValidationInfo from typing import Optional, List from datetime import datetime, date from decimal import Decimal @@ -7,12 +7,15 @@ class PropertyFilters(BaseModel): """Filters for property search""" + # Location filters city: Optional[str] = None state: Optional[str] = None zip: Optional[str] = None county: Optional[str] = None - bounds: Optional[List[float]] = Field(None, description="[west, south, east, north]") + bounds: Optional[List[float]] = Field( + None, description="[west, south, east, north]" + ) territory: Optional[dict] = None # GeoJSON Polygon # Property type @@ -82,16 +85,19 @@ class SolarFitData(BaseModel): class Config: from_attributes = True - @validator('shading_analysis', pre=True, always=True) - def build_shading_analysis(cls, v, values): + @field_validator("shading_analysis", mode="before") + @classmethod + def build_shading_analysis(cls, v, info: ValidationInfo): """Build shading_analysis dict from individual fields""" if v is not None: return v + # In Pydantic v2, access other fields via info.data instead of the values parameter + values = info.data return { - 'spring': values.get('shading_spring'), - 'summer': values.get('shading_summer'), - 'fall': values.get('shading_fall'), - 'winter': values.get('shading_winter'), + "spring": values.get("shading_spring"), + "summer": values.get("shading_summer"), + "fall": values.get("shading_fall"), + "winter": values.get("shading_winter"), } diff --git a/backend/app/tasks/google_ads_tasks.py b/backend/app/tasks/google_ads_tasks.py index d46d482..1fb3bf5 100644 --- a/backend/app/tasks/google_ads_tasks.py +++ b/backend/app/tasks/google_ads_tasks.py @@ -3,6 +3,7 @@ Celery tasks for syncing audiences to Google Ads Customer Match. """ + from datetime import datetime, timedelta from typing import Optional from sqlalchemy import select, and_ @@ -11,6 +12,7 @@ from app.tasks.celery_app import celery_app from app.core.database import AsyncSessionLocal +from app.core.config import settings from app.models.google_ads import ( GoogleAdsAccount, CustomerMatchAudience, @@ -27,6 +29,7 @@ def sync_audience_to_google_ads(audience_id: str, sync_id: Optional[str] = None): """Sync audience to Google Ads Customer Match""" import asyncio + return asyncio.run(_sync_audience_to_google_ads(audience_id, sync_id)) @@ -74,10 +77,15 @@ async def _sync_audience_to_google_ads(audience_id: str, sync_id: Optional[str] await session.commit() # Refresh access token if needed - if account.token_expires_at and account.token_expires_at < datetime.utcnow(): + if ( + account.token_expires_at + and account.token_expires_at < datetime.utcnow() + ): try: - new_token, expires_at = await google_ads_service.refresh_access_token( - account.refresh_token + new_token, expires_at = ( + await google_ads_service.refresh_access_token( + account.refresh_token + ) ) account.access_token = new_token account.token_expires_at = expires_at @@ -89,19 +97,23 @@ async def _sync_audience_to_google_ads(audience_id: str, sync_id: Optional[str] # Create user list in Google Ads if not exists if not audience.user_list_resource_name: try: - resource_name, user_list_id = await google_ads_service.create_customer_match_user_list( - customer_id=account.customer_id, - access_token=account.access_token, - refresh_token=account.refresh_token, - name=audience.name, - description=audience.description or "", + resource_name, user_list_id = ( + await google_ads_service.create_customer_match_user_list( + customer_id=account.customer_id, + access_token=account.access_token, + refresh_token=account.refresh_token, + name=audience.name, + description=audience.description or "", + ) ) audience.user_list_resource_name = resource_name audience.user_list_id = user_list_id await session.commit() - logger.info(f"Created user list {resource_name} for audience {audience_id}") + logger.info( + f"Created user list {resource_name} for audience {audience_id}" + ) except Exception as e: logger.error(f"Failed to create user list: {str(e)}") @@ -206,9 +218,10 @@ async def _sync_audience_to_google_ads(audience_id: str, sync_id: Optional[str] logger.error(f"Failed to upload contacts: {str(e)}") raise - # Wait a bit then get stats from Google Ads + # Wait for Google Ads to process upload before fetching stats import asyncio - await asyncio.sleep(5) + + await asyncio.sleep(settings.GOOGLE_ADS_SYNC_DELAY_SECONDS) try: stats = await google_ads_service.get_user_list_stats( @@ -276,6 +289,7 @@ async def _sync_audience_to_google_ads(audience_id: str, sync_id: Optional[str] def process_auto_sync_audiences(): """Process audiences with auto-sync enabled""" import asyncio + return asyncio.run(_process_auto_sync_audiences()) diff --git a/frontend/app/(dashboard)/comparison/page.tsx b/frontend/app/(dashboard)/comparison/page.tsx index 468ee38..c4d9500 100644 --- a/frontend/app/(dashboard)/comparison/page.tsx +++ b/frontend/app/(dashboard)/comparison/page.tsx @@ -1,6 +1,5 @@ 'use client' -import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' @@ -11,7 +10,7 @@ import { ArrowLeftRight, TrendingUp } from 'lucide-react' export default function PropertyComparisonPage() { // Demo: compare first 3 properties - const [propertyIds] = useState([]) + const propertyIds = ['property1', 'property2', 'property3'] const { data, isLoading } = useQuery({ queryKey: ['property-comparison', propertyIds],