Deduplicator9000
Servicio webhook que detecta negocios duplicados en HubSpot por direccion: normaliza la direccion con IA y geocoding, busca coincidencias entre objetos y publica un informe como nota.
Endpoints
GET/api/healthhealthGET/api/metrics/menumetrics menuGET/api/metricsmetricsCapabilities
apicron
Variables de entorno
HUBSPOT_API_KEYToken de la app privada de HubSpot (Bearer) con scopes de CRM para deals, custom objects y notasHUBSPOT_APP_SECRETClient secret de la app de HubSpot; valida la firma v3 del webhook entranteGEOCODING_API_KEYAPI key de Google Geocoding API (excepcion de stack documentada en docs.md)Documentacion
# Deduplicator9000 Servicio interno que detecta negocios duplicados en HubSpot por similitud de direccion. Cuando una persona cambia la direccion de un Deal o de un custom object, el servicio normaliza la direccion con IA (Gemini) y Google Geocoding, la guarda en `direccion_normalizada`, busca coincidencias entre 4 objetos del CRM y publica un informe de posibles duplicados como nota en el record. ## Como funciona 1. HubSpot envia un webhook `object.propertyChange` a `POST /api/webhook/hubspot`. 2. El servicio valida la firma v3 de HubSpot (HMAC) y filtra el evento (objeto valido + propiedad de direccion). 3. En background: corrige la direccion con Gemini -> geocoding -> guarda `direccion_normalizada` en el record. 4. Busca coincidencias de `direccion_normalizada` en Negocios, Comunidades, Comunidades con otro software y Postulando a comunidades. 5. Genera un "Certificado de Duplicidades" con Gemini y lo postea como nota asociada al record. Loop-protection: si el ultimo cambio de la propiedad lo hizo la propia app, o el `contador_deduplicator` supera 10, el evento se descarta. ## Objetos de HubSpot | Objeto | type_id | Propiedad | | ----------------------------- | ---------- | --------------------------------- | | Negocios (Deals) | 0-3 | calle_numero | | Comunidades | 2-26552810 | calle\_\_\_numero (solo se busca) | | Comunidades con otro software | 2-38809341 | calle\_\_\_numero | | Postulando a comunidades | 2-38812538 | calle\_\_\_numero | ## Endpoints | Metodo | Ruta | Auth | Descripcion | | ------ | ------------------------------- | ------------------- | --------------------------------- | | POST | /api/webhook/hubspot | Firma v3 de HubSpot | Receptor del webhook | | GET | /api/health | Publico | Health check | | GET | /api/metrics, /api/metrics/menu | x-api-key | Metricas para el Hub | | GET | /api/cron/backfill-direccion | x-api-key | Backfill de direccion_normalizada | ## Variables de entorno | Variable | Descripcion | | ------------------ | ------------------------------------------------------------------ | | HUBSPOT_API_KEY | Token de la app privada de HubSpot (Bearer) | | HUBSPOT_APP_SECRET | Client secret de la app de HubSpot; valida la firma v3 del webhook | | GEOCODING_API_KEY | API key de Google Geocoding API | Gemini usa la service account del Hub (`HUB_GOOGLE_SA_CREDENTIALS`), inyectada por el Hub; no requiere API key propia. ## Excepcion de stack: Google Geocoding API El stack aprobado no incluye APIs externas de Google salvo casos puntuales. Este servicio usa **Google Geocoding API** (`maps.googleapis.com`) para normalizar y geolocalizar direcciones (mayormente chilenas), funcion central del producto. Requiere `GEOCODING_API_KEY` y la aprobacion del owner del Hub antes de deployar. ## Backfill El `backfill.py` del proyecto original se migro a un cron idempotente (`/api/cron/backfill-direccion`, protegido con `x-api-key`): recorre los custom objects con direccion pero sin `direccion_normalizada`, normaliza en lotes (Gemini + geocoding) y actualiza HubSpot. Correrlo manualmente: `GET /api/cron/backfill-direccion` con el header `x-api-key`. ## Notas de la migracion - Origen: servicio Flask en Cloud Run (`--allow-unauthenticated`). El webhook no validaba el origen; ahora valida la firma v3 de HubSpot. - Secrets: se dejo de usar Google Secret Manager; las variables las inyecta el Hub. - El procesamiento pesado corre en background (el server de Railway es persistente), equivalente al thread del servicio original.