- Add is_banner, soldout fields to Car model
- Add banner toggle API (POST /hero-banners/admin/toggle/{car_id})
- Add soldout APIs (POST/DELETE /cars/{car_id}/soldout)
- Add nightly soldout checker in agent (runs at 3:00 AM)
- Update Local Cars UI with banner checkbox and status column
- Remove hero-banners admin page (functionality moved to Cars page)
- Banner cars sorted to top with purple background
- Soldout cars displayed with gray overlay
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
291 lines
9.4 KiB
Python
291 lines
9.4 KiB
Python
"""
|
|
Carmodoo Sync Agent - Syncs makers/models from Carmodoo to AutonetSellCar Backend
|
|
Also performs nightly soldout checks.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import logging
|
|
import httpx
|
|
from datetime import datetime, time
|
|
from dotenv import load_dotenv
|
|
from .carmodoo_client import CarmodooClient, CarmodooConfig
|
|
|
|
# Setup logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger('sync_agent')
|
|
|
|
|
|
class SyncAgent:
|
|
def __init__(self):
|
|
load_dotenv()
|
|
|
|
# Carmodoo config
|
|
self.carmodoo_config = CarmodooConfig(
|
|
user_id=os.getenv('CARMODOO_USER_ID', ''),
|
|
password=os.getenv('CARMODOO_PASSWORD', ''),
|
|
)
|
|
|
|
# Backend API
|
|
self.api_url = os.getenv('API_SERVER_URL', 'http://autonet-backend:8000/api')
|
|
self.api_key = os.getenv('AGENT_API_KEY', '')
|
|
|
|
self.carmodoo: CarmodooClient = None
|
|
self.http_client: httpx.AsyncClient = None
|
|
|
|
async def start(self):
|
|
"""Initialize connections"""
|
|
logger.info("Starting Sync Agent...")
|
|
|
|
self.carmodoo = CarmodooClient(self.carmodoo_config)
|
|
await self.carmodoo.create_session()
|
|
|
|
self.http_client = httpx.AsyncClient(timeout=30.0)
|
|
|
|
# Login to Carmodoo
|
|
if not await self.carmodoo.login():
|
|
logger.error("Failed to login to Carmodoo")
|
|
return False
|
|
|
|
logger.info("Sync Agent started successfully")
|
|
return True
|
|
|
|
async def stop(self):
|
|
"""Cleanup connections"""
|
|
if self.carmodoo:
|
|
await self.carmodoo.close()
|
|
if self.http_client:
|
|
await self.http_client.aclose()
|
|
logger.info("Sync Agent stopped")
|
|
|
|
async def sync_makers(self):
|
|
"""Sync car makers from Carmodoo to Backend"""
|
|
logger.info("Syncing car makers...")
|
|
|
|
makers = await self.carmodoo.get_car_makers()
|
|
logger.info(f"Found {len(makers)} makers from Carmodoo")
|
|
|
|
synced = 0
|
|
for maker in makers:
|
|
try:
|
|
response = await self.http_client.post(
|
|
f"{self.api_url}/cars/makers/",
|
|
json={
|
|
"code": maker.code,
|
|
"name": maker.name,
|
|
}
|
|
)
|
|
if response.status_code in [200, 201]:
|
|
synced += 1
|
|
except Exception as e:
|
|
logger.error(f"Error syncing maker {maker.code}: {e}")
|
|
|
|
logger.info(f"Synced {synced}/{len(makers)} makers")
|
|
return makers
|
|
|
|
async def sync_models(self, makers):
|
|
"""Sync car models for all makers"""
|
|
logger.info("Syncing car models...")
|
|
|
|
total_models = 0
|
|
synced = 0
|
|
|
|
for maker in makers:
|
|
await asyncio.sleep(0.5) # Rate limiting
|
|
|
|
models = await self.carmodoo.get_car_models(maker.code)
|
|
total_models += len(models)
|
|
|
|
# Get maker ID from backend
|
|
try:
|
|
response = await self.http_client.get(f"{self.api_url}/cars/makers/")
|
|
if response.status_code == 200:
|
|
backend_makers = response.json()
|
|
maker_id = None
|
|
for bm in backend_makers:
|
|
if bm['code'] == maker.code:
|
|
maker_id = bm['id']
|
|
break
|
|
|
|
if maker_id:
|
|
for model in models:
|
|
try:
|
|
resp = await self.http_client.post(
|
|
f"{self.api_url}/cars/models/",
|
|
json={
|
|
"code": model.code,
|
|
"maker_id": maker_id,
|
|
"name": model.name,
|
|
}
|
|
)
|
|
if resp.status_code in [200, 201]:
|
|
synced += 1
|
|
except Exception as e:
|
|
logger.error(f"Error syncing model {model.code}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Error getting makers from backend: {e}")
|
|
|
|
logger.debug(f"Maker {maker.name}: {len(models)} models")
|
|
|
|
logger.info(f"Synced {synced}/{total_models} models")
|
|
|
|
async def run_sync(self):
|
|
"""Run full sync"""
|
|
if not await self.start():
|
|
return
|
|
|
|
try:
|
|
# Sync makers
|
|
makers = await self.sync_makers()
|
|
|
|
# Sync models
|
|
await self.sync_models(makers)
|
|
|
|
logger.info("Sync completed successfully!")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Sync error: {e}")
|
|
finally:
|
|
await self.stop()
|
|
|
|
async def check_soldout(self):
|
|
"""Check all cars for soldout status"""
|
|
logger.info("Starting soldout check...")
|
|
|
|
if not await self.start():
|
|
return
|
|
|
|
try:
|
|
# Get all active cars from backend
|
|
response = await self.http_client.get(
|
|
f"{self.api_url}/cars",
|
|
params={"admin": True, "page_size": 1000, "status": "active"}
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
logger.error(f"Failed to get cars: {response.status_code}")
|
|
return
|
|
|
|
data = response.json()
|
|
cars = data.get("cars", [])
|
|
logger.info(f"Checking {len(cars)} cars for soldout status...")
|
|
|
|
soldout_count = 0
|
|
checked = 0
|
|
|
|
for car in cars:
|
|
if car.get("soldout"):
|
|
continue # Already soldout
|
|
|
|
source_id = car.get("source_id")
|
|
car_id = car.get("id")
|
|
|
|
if not source_id:
|
|
continue
|
|
|
|
# Check if car exists on Carmodoo
|
|
is_available = await self._check_car_on_carmodoo(source_id)
|
|
checked += 1
|
|
|
|
if not is_available:
|
|
# Mark as soldout via API
|
|
try:
|
|
resp = await self.http_client.post(
|
|
f"{self.api_url}/cars/{car_id}/soldout"
|
|
)
|
|
if resp.status_code == 200:
|
|
soldout_count += 1
|
|
logger.info(f"Car {car_id} ({car.get('car_name')}) marked as SOLD OUT")
|
|
except Exception as e:
|
|
logger.error(f"Failed to mark car {car_id} as soldout: {e}")
|
|
|
|
# Rate limiting
|
|
if checked % 10 == 0:
|
|
logger.info(f"Progress: {checked}/{len(cars)} checked, {soldout_count} sold out")
|
|
await asyncio.sleep(1)
|
|
|
|
logger.info(f"Soldout check completed: {checked} checked, {soldout_count} sold out")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Soldout check error: {e}")
|
|
finally:
|
|
await self.stop()
|
|
|
|
async def _check_car_on_carmodoo(self, source_id: str) -> bool:
|
|
"""Check if car exists on Carmodoo"""
|
|
try:
|
|
# Try to get car info from Carmodoo
|
|
url = f"https://dealer.carmodoo.com/car/carPopView.html"
|
|
params = {"carNo": source_id}
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.get(url, params=params)
|
|
|
|
if response.status_code == 404:
|
|
return False
|
|
|
|
content = response.text.lower()
|
|
sold_keywords = ["판매완료", "판매 완료", "삭제된", "없는 차량", "존재하지 않"]
|
|
|
|
for keyword in sold_keywords:
|
|
if keyword in content:
|
|
return False
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking car {source_id}: {e}")
|
|
return True # Assume available on error
|
|
|
|
async def run_scheduled(self):
|
|
"""Run agent with scheduled tasks"""
|
|
logger.info("Starting scheduled agent...")
|
|
|
|
# Run initial sync
|
|
await self.run_sync()
|
|
|
|
# Schedule nightly soldout check at 3:00 AM
|
|
while True:
|
|
now = datetime.now()
|
|
target_time = time(3, 0) # 3:00 AM
|
|
|
|
# Calculate seconds until next 3:00 AM
|
|
target_datetime = datetime.combine(now.date(), target_time)
|
|
if now.time() >= target_time:
|
|
# Already past 3 AM today, schedule for tomorrow
|
|
from datetime import timedelta
|
|
target_datetime += timedelta(days=1)
|
|
|
|
seconds_until = (target_datetime - now).total_seconds()
|
|
logger.info(f"Next soldout check in {seconds_until / 3600:.1f} hours at {target_datetime}")
|
|
|
|
await asyncio.sleep(seconds_until)
|
|
|
|
# Run soldout check
|
|
logger.info("Running scheduled soldout check...")
|
|
await self.check_soldout()
|
|
|
|
|
|
async def main():
|
|
agent = SyncAgent()
|
|
|
|
# Check command line args
|
|
import sys
|
|
if len(sys.argv) > 1:
|
|
if sys.argv[1] == "sync":
|
|
await agent.run_sync()
|
|
elif sys.argv[1] == "soldout":
|
|
await agent.check_soldout()
|
|
elif sys.argv[1] == "scheduled":
|
|
await agent.run_scheduled()
|
|
else:
|
|
# Default: run scheduled
|
|
await agent.run_scheduled()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
asyncio.run(main())
|