Álvaro Valenzuela Valdes
feat: integrate buyer risk intelligence and scraped tender details into agent analysis
26fba59 | from sqlalchemy.orm import Session | |
| from datetime import datetime | |
| from app.models.tender import TenderModel | |
| from app.models.oc import OCModel | |
| from app.services.mercado_publico import fetch_tenders, get_tender_by_code | |
| from app.services.mercado_publico_oc import get_ocs_by_date | |
| import json | |
| async def sync_tenders_to_db(db: Session, keyword: str = None): | |
| """ | |
| Fetches real tenders from Mercado Público API and saves them. | |
| """ | |
| print(f"[Sync] Starting REAL synchronization... keyword={keyword}") | |
| try: | |
| api_tenders = await fetch_tenders(keyword=keyword) | |
| if not api_tenders: | |
| print("[Sync] No active tenders found for today in the API.") | |
| return {"new": 0, "updated": 0, "message": "No new tenders found"} | |
| print(f"[Sync] API returned {len(api_tenders)} real tenders for processing.") | |
| except Exception as e: | |
| print(f"[Sync] API error: {e}") | |
| return {"new": 0, "updated": 0, "message": f"API Error: {str(e)}"} | |
| count_new = 0 | |
| count_updated = 0 | |
| # Deduplicate API results by code to avoid IntegrityError within the same batch | |
| seen_codes = set() | |
| unique_tenders = [] | |
| for t in api_tenders: | |
| if t.code not in seen_codes: | |
| seen_codes.add(t.code) | |
| unique_tenders.append(t) | |
| for api_t in unique_tenders: | |
| # Check if exists | |
| db_tender = db.query(TenderModel).filter(TenderModel.code == api_t.code).first() | |
| # Helper to parse dates | |
| def parse_dt(dt_str): | |
| if not dt_str: return None | |
| try: | |
| # Handle Z and other common formats | |
| clean_str = dt_str.replace("Z", "").split(".")[0] | |
| return datetime.fromisoformat(clean_str) | |
| except: | |
| return None | |
| # Convert Pydantic model to dict for DB | |
| tender_data = { | |
| "code": api_t.code, | |
| "name": api_t.name, | |
| "buyer": api_t.buyer, | |
| "buyer_region": api_t.buyer_region, | |
| "status": api_t.status, | |
| "status_code": str(api_t.status_code) if api_t.status_code else None, | |
| "type": api_t.type, | |
| "currency": api_t.currency, | |
| "closing_date": parse_dt(api_t.closing_date) if isinstance(api_t.closing_date, str) else api_t.closing_date, | |
| "publication_date": parse_dt(api_t.publication_date) if isinstance(api_t.publication_date, str) else api_t.publication_date, | |
| "description": api_t.description, | |
| "estimated_amount": api_t.estimated_amount, | |
| "source": api_t.source, | |
| "region": api_t.region, | |
| "sector": api_t.sector, | |
| "items": [item.model_dump() for item in api_t.items] if api_t.items else [], | |
| "attachments": api_t.attachments, | |
| "evaluation_criteria": api_t.evaluation_criteria, | |
| "contract_duration": api_t.contract_duration | |
| } | |
| if db_tender: | |
| # Update existing | |
| for key, value in tender_data.items(): | |
| setattr(db_tender, key, value) | |
| count_updated += 1 | |
| else: | |
| # Create new | |
| new_tender = TenderModel(**tender_data) | |
| db.add(new_tender) | |
| count_new += 1 | |
| db.commit() | |
| print(f"[Sync] Finished. New: {count_new}, Updated: {count_updated}") | |
| return {"new": count_new, "updated": count_updated} | |
| async def sync_purchase_orders_to_db(db: Session, date: str = None, status: str = "todos"): | |
| """ | |
| Fetches purchase orders from Mercado Público and saves them in the local database. | |
| """ | |
| if not date: | |
| date = datetime.now().strftime("%d%m%Y") | |
| try: | |
| api_orders = await get_ocs_by_date(date, status) | |
| if not api_orders: | |
| print(f"[Sync OC] No purchase orders found for date={date} status={status}") | |
| return {"new": 0, "updated": 0, "message": "No purchase orders found"} | |
| except Exception as e: | |
| print(f"[Sync OC] API error: {e}") | |
| return {"new": 0, "updated": 0, "message": f"API Error: {str(e)}"} | |
| count_new = 0 | |
| count_updated = 0 | |
| seen_codes = set() | |
| for oc in api_orders: | |
| if oc.code in seen_codes: | |
| continue | |
| seen_codes.add(oc.code) | |
| db_oc = db.query(OCModel).filter(OCModel.code == oc.code).first() | |
| oc_data = { | |
| "code": oc.code, | |
| "name": oc.name, | |
| "status": oc.status, | |
| "status_code": oc.status_code, | |
| "buyer": oc.buyer, | |
| "buyer_rut": oc.buyer_rut, | |
| "provider": oc.provider, | |
| "provider_rut": oc.provider_rut, | |
| "date_creation": oc.date_creation, | |
| "total_amount": oc.total_amount, | |
| "currency": oc.currency, | |
| "type": oc.type, | |
| "items": [item.model_dump() for item in oc.items] if oc.items else [], | |
| "raw_data": oc.raw_data, | |
| } | |
| if db_oc: | |
| for key, value in oc_data.items(): | |
| setattr(db_oc, key, value) | |
| count_updated += 1 | |
| else: | |
| new_oc = OCModel(**oc_data) | |
| db.add(new_oc) | |
| count_new += 1 | |
| db.commit() | |
| print(f"[Sync OC] Finished. New: {count_new}, Updated: {count_updated}") | |
| return {"new": count_new, "updated": count_updated} | |
| def clean_expired_tenders(db: Session): | |
| """ | |
| Removes tenders where closing_date is in the past. | |
| """ | |
| now = datetime.now() | |
| expired = db.query(TenderModel).filter(TenderModel.closing_date < now).delete() | |
| db.commit() | |
| print(f"[Sync] Cleaned {expired} expired tenders.") | |
| return expired | |