| from datetime import datetime |
| from typing import List, Optional |
| from fastapi import APIRouter, Query, Depends |
| from sqlalchemy.orm import Session |
| from sqlalchemy import or_ |
|
|
| from app.schemas.tender import Tender |
| from app.database import get_db |
| from app.models.tender import TenderModel |
| from app.services.sync import sync_tenders_to_db, clean_expired_tenders |
| from app.services.mercado_publico import ( |
| fetch_tenders, |
| get_tender_by_code, |
| get_tenders_by_date, |
| ) |
| from app.models.company import CompanyProfileModel |
| import json |
|
|
| router = APIRouter() |
|
|
| @router.get("/tenders", response_model=List[Tender]) |
| async def search_tender_opportunities( |
| keyword: Optional[str] = None, |
| buyer: Optional[str] = None, |
| region: Optional[str] = None, |
| provider_code: Optional[str] = Query(None, alias="provider_code"), |
| org_code: Optional[str] = Query(None, alias="org_code"), |
| status: Optional[str] = None, |
| code: Optional[str] = None, |
| date: Optional[str] = None, |
| type_code: Optional[str] = Query(None, alias="type_code"), |
| skip: int = 0, |
| limit: int = 50, |
| db: Session = Depends(get_db) |
| ): |
| |
| if code: |
| tender = await get_tender_by_code(code) |
| return [tender] if tender else [] |
|
|
| if any([provider_code, org_code, status, date, type_code]) and not keyword: |
| from app.services.mercado_publico import get_tenders_by_filters |
| return await get_tenders_by_filters( |
| date=date, |
| status=status, |
| type_code=type_code, |
| org_code=org_code, |
| provider_code=provider_code |
| ) |
|
|
| if keyword: |
| from app.services.mercado_publico import fetch_tenders |
| return await fetch_tenders(keyword=keyword, date=date, type_code=type_code) |
|
|
| |
| query = db.query(TenderModel) |
| |
| if keyword: |
| search_filter = f"%{keyword}%" |
| query = query.filter( |
| or_( |
| TenderModel.name.ilike(search_filter), |
| TenderModel.code.ilike(search_filter), |
| TenderModel.description.ilike(search_filter), |
| TenderModel.buyer.ilike(search_filter), |
| TenderModel.sector.ilike(search_filter), |
| TenderModel.region.ilike(search_filter) |
| ) |
| ) |
| |
| if buyer: |
| query = query.filter(TenderModel.buyer.ilike(f"%{buyer}%")) |
| |
| if region: |
| query = query.filter(TenderModel.region.ilike(f"%{region}%")) |
| |
| |
| results = query.order_by(TenderModel.closing_date.asc()).offset(skip).limit(limit).all() |
| |
| |
| |
| |
| if not results: |
| print(f"[Tenders] No results in DB. Triggering sync. keyword={keyword}") |
| await sync_tenders_to_db(db, keyword=keyword) |
| |
| results = query.offset(skip).limit(limit).all() |
| |
| return results |
|
|
| @router.get("/tenders/count") |
| def get_tenders_count(db: Session = Depends(get_db)): |
| """Devuelve el total de licitaciones en la base de datos.""" |
| return {"total": db.query(TenderModel).count()} |
|
|
| @router.post("/tenders/sync") |
| async def manual_sync(keyword: Optional[str] = None, db: Session = Depends(get_db)): |
| return await sync_tenders_to_db(db, keyword=keyword) |
|
|
| @router.get("/tenders/scrape", response_model=List[Tender]) |
| async def live_scrape(keyword: str): |
| from app.services.scraper import scrape_compra_agil |
| return await scrape_compra_agil(keyword) |
|
|
| @router.get("/tenders/recommendations", response_model=List[Tender]) |
| async def get_recommended_tenders(db: Session = Depends(get_db)): |
| """Busca licitaciones locales que coincidan con las keywords del perfil de empresa.""" |
| print("!!! RECOMMENDATION ENDPOINT CALLED !!!") |
| profile = db.query(CompanyProfileModel).first() |
| |
| |
| if not profile or not profile.keywords: |
| print("No profile or keywords found, returning latest 10") |
| return db.query(TenderModel).order_by(TenderModel.closing_date.desc()).limit(10).all() |
| |
| try: |
| |
| if profile.keywords.startswith("[") or profile.keywords.startswith("{"): |
| keywords = json.loads(profile.keywords) |
| else: |
| keywords = [kw.strip() for kw in profile.keywords.split(",") if kw.strip()] |
| except Exception as e: |
| print(f"Keyword parse error: {e}") |
| keywords = [profile.keywords] if profile.keywords else [] |
|
|
| print(f"Processing keywords: {keywords}") |
|
|
| |
| filters = [] |
| for kw in keywords: |
| if not kw or len(kw) < 2: continue |
| search_term = f"%{kw}%" |
| filters.append(TenderModel.name.ilike(search_term)) |
| filters.append(TenderModel.description.ilike(search_term)) |
| filters.append(TenderModel.buyer.ilike(search_term)) |
| filters.append(TenderModel.sector.ilike(search_term)) |
|
|
| |
| if not filters: |
| print("No valid filters generated, returning latest 10") |
| return db.query(TenderModel).order_by(TenderModel.closing_date.desc()).limit(10).all() |
|
|
| |
| try: |
| recommended = db.query(TenderModel).filter(or_(*filters)).order_by(TenderModel.closing_date.desc()).limit(15).all() |
| print(f"Found {len(recommended)} recommended matches") |
| except Exception as e: |
| print(f"Query error: {e}") |
| recommended = [] |
| |
| |
| if not recommended: |
| print("No matches found, executing fallback to latest 10") |
| recommended = db.query(TenderModel).order_by(TenderModel.closing_date.desc()).limit(10).all() |
| elif len(recommended) < 5: |
| print(f"Only {len(recommended)} found, padding with latest") |
| existing_ids = [r.id for r in recommended] |
| more = db.query(TenderModel).filter(TenderModel.id.not_in(existing_ids)).order_by(TenderModel.closing_date.desc()).limit(5).all() |
| recommended.extend(more) |
|
|
| return recommended |
|
|