Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions backend/app/schemas/property.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"),
}


Expand Down
38 changes: 26 additions & 12 deletions backend/app/tasks/google_ads_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand All @@ -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,
Expand All @@ -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))


Expand Down Expand Up @@ -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
Expand All @@ -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)}")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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())


Expand Down
3 changes: 1 addition & 2 deletions frontend/app/(dashboard)/comparison/page.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,7 +10,7 @@ import { ArrowLeftRight, TrendingUp } from 'lucide-react'

export default function PropertyComparisonPage() {
// Demo: compare first 3 properties
const [propertyIds] = useState<string[]>([])
const propertyIds = ['property1', 'property2', 'property3']

const { data, isLoading } = useQuery({
queryKey: ['property-comparison', propertyIds],
Expand Down