Skip to content

faborubio/oteo

Repository files navigation

Oteo

Dashboard de prospección de negocios locales del Maule sin presencia digital.
Recorre Google Places por comuna × rubro, clasifica la presencia digital de cada negocio y su candidatura a POS, y lo convierte en un pipeline de ventas accionable.

CI Ruby 3.3 Rails 8.1 PostgreSQL 16 RSpec En producción

🚀 En producción desde julio 2026 — ~2.400 negocios de 12 comunas clasificados, sync quincenal automático, backups diarios cifrados con restauración probada, operación dentro del free tier de Google Places (deploy: Kamal 2 sobre una VM de GCE).


¿Qué es Oteo?

Otear es escudriñar el horizonte desde lo alto en busca de algo. Oteo otea el territorio comercial del Maule: encuentra negocios con buena reputación (muchas reseñas) pero sin web propia — el lead ideal para vender un sitio web — y detecta candidatos para un sistema POS, un dato que Google no tiene y que se captura en terreno.

El problema que resuelve: prospectar a mano en Google Maps significa abrir ficha por ficha, verificar si hay web y anotar en una planilla. La señal que convierte —negocio con reputación y sin presencia digital— existe en Places pero no es filtrable desde Maps. Oteo la extrae, la clasifica y la ordena por prioridad de venta.

Es una herramienta interna (la usa un equipo de 3 personas en terreno), construida con disciplina de producto: cada decisión de arquitectura está documentada como ADR en el SAD, con su contexto y trade-offs.

Características

  • 🔎 Sincronización desde Google Places API (New) con un adapter que aísla al proveedor y minimiza costo: todos los campos se piden en el field mask del Text Search, nunca Place Details por lugar (~180 llamadas por sync completo de 96 combinaciones vs. miles del enfoque ingenuo — el sync quincenal usa ~36% del cupo gratuito).
  • 🏷️ Clasificación de presencia digital en tres estadossin_presencia / solo_redes / web_propia — donde redes sociales y agregadores de delivery no cuentan como web propia (cada estado tiene su guion de venta).
  • 🎯 Candidatura POS híbrida: heurística automática por rubro + confirmación manual en terreno; el dato observado siempre manda sobre el inferido.
  • 📈 Lead score explícito y ajustable: log(1 + reseñas) × peso_presencia × bonus_POS. La tabla se ordena por prioridad de prospección.
  • 🔁 Pipeline idempotente: re-sincronizar nunca duplica filas ni pisa el dato de terreno (notas, etapa del pipeline, POS observado).
  • 🔍 Buscador insensible a tildes (extensión unaccent de Postgres) y alta manual de negocios que Google no lista — con clasificación automática al crear.
  • 💰 Costo cero de operación: free tier de Places + Solid Queue/Cache/Cable sobre PostgreSQL (sin Redis ni servicios pagados). Backups diarios cifrados (age) fuera del VPS, con restauración probada antes de confiar en ellos.

Arquitectura

Un pipeline batch simple — sincronizar → clasificar → persistir — y tres vistas sobre la misma tabla businesses:

  Solid Queue                Google Places API (New)
  SyncJob(comuna, rubro) ──▶  PlacesClient (adapter, field mask mínimo)
                                        │  emite Snapshot normalizado
                                        ▼
                              BusinessClassifier
                              ├─ PresenceClassifier  (ADR-003)
                              ├─ PosCandidateClassifier (ADR-004)
                              └─ LeadScorer          (ADR-008)
                                        │
                                        ▼
                     PostgreSQL: businesses · business_rubros
                               sync_runs · contact_events
                                        │
                 ┌──────────────────────┼──────────────────────┐
                 ▼                      ▼                      ▼
             Tabla filtrable      Mapa (Google Maps)      Kanban CRM
             (Turbo Frames)         por estado            (Turbo Streams)

El dominio no sabe de Google. PlacesClient es un adapter tras una interfaz: los clasificadores y modelos consumen un Snapshot normalizado, no el JSON de Google. Si Places cambia de versión o se agrega otra fuente, el dominio no se toca. En tests el adapter se sustituye con WebMock — cero llamadas reales en CI.

Stack

Ruby 3.3 · Rails 8.1 · PostgreSQL 16 (jsonb) · Hotwire (Turbo + Stimulus) · Tailwind CSS · Solid Queue/Cache/Cable · Kamal 2 · RSpec · RuboCop (omakase) · Brakeman · bundler-audit.

Puesta en marcha

Requisitos

  • Ruby 3.3, PostgreSQL 16 corriendo localmente.
  • Una API key de Google Places API (New) para sincronizar datos reales (opcional para dev/tests).

Instalación

git clone https://github.com/faborubio/oteo.git
cd oteo
bundle install                 # instala gems en vendor/bundle (ver nota abajo)
bin/rails db:create db:migrate

# Usuario admin + taxonomías (12 comunas × 8 rubros = 96 combinaciones)
OTEO_ADMIN_EMAIL=tu@correo.cl OTEO_ADMIN_PASSWORD=secreto bin/rails db:seed

bin/dev                        # levanta web + watcher de Tailwind

Nota: en este entorno los gems del sistema no son escribibles, así que bundler instala en vendor/bundle (.bundle/config). Consecuencia: usar siempre bundle exec (o bin/*).

Variables de entorno

Variable Uso
GOOGLE_PLACES_API_KEY Autenticación con Places API (restringir por API + IP en producción)
OTEO_ADMIN_EMAIL / OTEO_ADMIN_PASSWORD Usuario único, usados por el seed

Uso

Sincronizar desde Places (requiere GOOGLE_PLACES_API_KEY):

# Sync síncrono de una combinación, ideal para auditar el primer resultado real
bundle exec rake 'oteo:sync_now[curico,restaurantes]'

# Encolar una combinación en Solid Queue
bundle exec rake 'oteo:sync_one[talca,botillerias]'

# Encolar TODAS las combinaciones activas
bundle exec rake oteo:sync_all

Cada corrida se audita en sync_runs (encontrados / nuevos / actualizados / errores y llamadas consumidas contra la cuota). Si la cuota se agota, el job aborta y alerta — jamás pasa a facturación.

Calidad

bundle exec rspec                 # suite completa (145 ejemplos)
COVERAGE=true bundle exec rspec   # con reporte de cobertura
bundle exec rubocop               # estilo (omakase)
bin/brakeman --no-pager           # análisis de seguridad estático
bin/bundler-audit                 # CVEs en dependencias

El CI (GitHub Actions) corre estos cuatro jobs — scan_ruby, scan_js, lint, test (con servicio PostgreSQL)— en cada push y PR.

Estructura del proyecto

app/
  models/         businesses, business_rubros, comunas, rubros, sync_runs, contact_events
  services/       PlacesClient (adapter) · clasificadores · BusinessClassifier (facade)
  jobs/           SyncJob (idempotente)
config/
  oteo.yml        dominios sociales, pesos del lead_score, ventana de retención ToS
docs/
  AUDIT.md        deuda técnica y gates legales (formato AUD-NNN)
  CASES.md        memoria del clasificador: URIs reales y qué se decidió
  TROUBLESHOOTING.md
SAD-Oteo.md       Software Architecture Document — la fuente de verdad
CLAUDE.md         contexto operativo para retomar el proyecto entre sesiones

Roadmap

Fase Alcance Estado
0 — Cimientos Scaffold, toolkit, auth, modelo de datos, adapter Places, taxonomías, CI ✅ Completa
1 — Pipeline de datos SyncJob idempotente + clasificadores + sync_runs, calibrado con 2 auditorías de datos reales ✅ Completa
2 — Las tres vistas Tabla filtrable + buscador, ficha + captura móvil de POS, kanban drag&drop, mapa (Google Maps JS), alta manual ✅ Completa
3 — Operación Sync programado quincenal, página de salud, expiración ToS, guiones de contacto, deploy + backups En producción (2026-07-09)
4 — Solo con tracción Verificación HTTP de webs; el resto espera en IDEAS.md

Documentación

  • SAD-Oteo.md — arquitectura completa, drivers, NFRs y todos los ADR con sus trade-offs. Es la fuente de verdad; el resto de docs deriva de aquí.
  • CLAUDE.md — contexto operativo para retomar el proyecto en una sesión nueva.
  • docs/AUDIT.md — deuda técnica explícita y gates legales, con estado.
  • docs/CASES.md — memoria del clasificador de presencia digital.
  • docs/SECURITY.md — modelo de amenaza, matriz de riesgo y postura proporcional; docs/DEPLOY.md — runbook de producción validado.
  • IDEAS.md — parking lot: ideas diferidas que esperan tracción.

Autor

Fabián Rubio — Full Stack. Herramienta interna; potencial producto futuro.

About

Dashboard de prospección de negocios del Maule sin presencia digital: recorre Google Places por comuna × rubro, clasifica presencia digital y candidatura a POS, y arma un pipeline de ventas accionable. Rails 8.1 + PostgreSQL.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors