Propósito
El sistema genera, por ciclo de facturación y por cliente, un dashboard embebido en WordPress (brandacare.com) con: dinero cobrado del seguro, A/R recuperado, verificaciones de seguro y no-shows. El widget es UNO SOLO y modular: prende/apaga tarjetas según el "tier" del cliente. Todo corre solo, a diario, con GitHub Actions, y se publica por SFTP a Cloudways.
Ficha 1 — Arquitectura, tiers y roster de clientes
Flujo general
Open Dental (API) + OPS Google Sheet (tab "ALL DATA", filtrado por "Doc")
▼
recovery_metrics.py
▼
output/metrics_<id>.json
▼
widget en /dashboards/ de Cloudways
▼
iframe en WordPress (gateado por email)
run_pipeline.py orquesta todos los clientes; GitHub Actions lo corre a diario y sube los JSON + el widget por SFTP.
Tiers (definen qué tarjetas ve el cliente)
| Tier | Tarjetas | Ejemplos |
|---|---|---|
billing+insurance (full) | Total recovered lifetime + A/R gráfico 6m + Insurance collected + A/R recovered + Verificaciones + No-shows | Hallandale, Casas |
billing+ar | Total recovered lifetime + A/R gráfico + Insurance collected + A/R recovered. SIN verificaciones | Benitez |
verifications (verif-only) | SOLO verificaciones + Solstice + No-shows. Oculta todo lo de dinero, aunque OD del cliente tenga esos números. Salta la extracción de pagos → corre rápido | A+ |
Roster de clientes (jun 2026)
| Cliente | Tier / Fuente | Estado dashboard | Notas |
|---|---|---|---|
| Hallandale Dental Care | billing+insurance (OD API) | EN VIVO | MVP |
| Benitez Dental Center | billing+ar (OD API) | EN VIVO | Lifetime A/R $13,192 |
| A+ Dental of Aventura | verifications (OD API) | EN VIVO | No-show por Unscheduled List |
| Dr Casas Family Dentistry | billing+insurance (OD API) | PENDIENTE | Esperar más A/R para reflejar el trabajo |
| G-Dental | Dentrix (sin API) | FUTURO | Cross-match OPS + reportes Dentrix |
| Le Dentiste | Dentrix (sin API) | FUTURO | Cross-match OPS + reportes Dentrix |
Ficha 2 — Métricas de dinero (recovery_metrics.py)
Las 3 cifras (definiciones exactas)
| Cifra | Definición |
|---|---|
| TOTAL RECOVERED (lifetime) | A/R recuperado acumulado desde service_start_date. |
| INSURANCE COLLECTED (ciclo) | TODO lo cobrado del seguro en el ciclo, sin importar antigüedad. |
| A/R RECOVERED (ciclo) | Subconjunto cobrado con DOS de 45+ días — claims viejos que requirieron seguimiento. |
Regla de "recuperado" (crystal clear)
- A/R recuperado = pago posteado MÁS de 45 días después del DOS (
CheckDate − ProcDate > 45). - El piso de fecha es sobre la FECHA DEL PAGO (
>= service_start_date), NO sobre el DOS. Un claim con DOS viejo (aun de Dentemax) cobrado durante nuestra gestión SÍ cuenta. - Código por código: cada claimproc cuenta literal. NO se deduplica por importe.
Fuentes y filtro
- Open Dental (
claimpayments → claimprocs) para pagos;appointmentspara citas. - OPS Google Sheet, tab "ALL DATA", FILTRADO por columna "Doc" =
ops_doc_name(el tab mezcla todos los clientes).
Flags nuevos
| Flag | Función |
|---|---|
Verif-only (tier=verifications) | Se SALTA od_insurance_payments (do_billing=False) → mucho más rápido y no muestra dinero. |
--as-of YYYY-MM-DD | Corta el conteo a esa fecha (snapshot del día, ej. para comparar con un reporte manual). |
Comando y outputs
python recovery_metrics.py --client <id> \
--ops-sheet <SHEET_ID> --ops-tab "ALL DATA" \
[--as-of 2026-06-28] --debug
output/metrics_<id>.json→ lo lee el widget. Incluye "tier" y "modules" (tarjetas on/off).- Auditoría CSV:
recovered_by_patient,recovered_lines,verifications_detail,broken_appts_detail.
Ficha 3 — Verificaciones de seguro
Qué cuenta
- Fuente: notas de cita OD con
INS ACTIVE/INACTIVE - BCUNIDAS con el OPS (columna "Result" = Active/Inactive). - SOLO pacientes que VINIERON (cita
AptStatus = Complete). Se excluyen no-shows, futuras y no confirmables. - Deduplicado por paciente (1 por paciente/ciclo).
Valor (referencia ADA — actualizado)
| Métrica | Valor | Nota |
|---|---|---|
| Tiempo ahorrado por verificación | 10 minutos | MINUTES_SAVED_PER_VERIFICATION = 10, igual que la web |
| Dinero ahorrado por verificación | $5.63 | ADA. Implica ~$33.78/hora — mayor que sueldo crudo ($20-22) porque captura errores/denials evitados |
| Editable por cliente | verification_value_per_usd | Config del cliente |
Solstice (cortesía) — NUEVO
- Se identifica por el carrier: el detalle de verificaciones ahora trae
"Ins Carrier"(del OPS) y un flag"Solstice?". solstice_count= verificados cuyo carrier contiene "solstice".- El carrier se cruza por paciente desde el OPS; puede no conocerse para verificaciones que sólo vienen de notas OD (sin match en OPS), así que
solstice_countes un piso.
Ficha 4 — No-shows (dos definiciones según el cliente)
Default (broken en el calendario)
- No-show = pacientes que VERIFICAMOS y luego no vinieron (cita
Brokenen OD). - Deduplicado por paciente.
- El JSON también trae
broken_appointments_all(todos los broken del ciclo, no sólo los verificados) para cotejo.
A+ y similares: noshow_source = "unscheduled" — NUEVO (clave)
AptStatus=UnschedList), que no tiene fecha de calendario, por eso el escaneo día-por-día daba 0.
fetch_od_unscheduled_noshows()traeAptStatus=UnschedListfiltrando porAptDateTime(el slot perdido) dentro del ciclo, condateStart/dateEndserver-side, y deduplica por paciente. Reproduce el conteo manual del cliente.- El OPS-total UnschedList tiene decenas de miles (tratamientos planificados históricos); el filtro por AptDateTime-en-ciclo aísla los no-shows reales del período.
Corte a HOY (no contar el futuro) — fix jul-2026
- En un ciclo EN CURSO se cuenta solo hasta hoy:
ce_eff = min(fin_de_ciclo, hoy). - Antes tomaba todo el ciclo (hasta el fin), y agarraba citas del Unscheduled con AptDateTime futuro (tratamientos planificados aún no ocurridos) como si fueran no-shows.
- Firma del falso positivo:
DateTStamp ANTERIOR al AptDateTime(cita editada antes de su fecha = futura). Un no-show real se marca en/después del día de la cita. - El flag
--as-ofusa la misma lógica para reproducir un snapshot de un día pasado.
Ficha 5 — Modelo A+ (verifications-only) — NUEVO
A+ paga SOLO verificaciones (no billing ni A/R). El dashboard muestra el número que paga bien claro y suma las cortesías para inflar el valor total, validando el precio.
| Categoría | Fórmula |
|---|---|
| Billable (lo que paga) | Verificaciones completadas − Solstice |
| Cortesía (no charge) | Solstice + No-shows (verificados, no vinieron) |
| Total verified | Completadas + no-shows |
La matemática de valor (horas y $) va sobre el TOTAL.
294 billable + 30 Solstice + 137 no-show = 461 total verified ≈ 76.8 hrs ≈ $2,595 (vs ~$1,000/mes que pagan → 2.4x)
Campos JSON
billable_verifications, solstice_count, broken_appointments, total_verified, verification_value_total_usd, hours_saved_total
Ficha 6 — A/R Recovery (procedimientos sin claim)
Encuentra procedimientos completados SIN claim en Open Dental (post-Dentemax), identifica los facturables y permite enviarlos al seguro.
| Script | Función |
|---|---|
find_unbilled_procedures.py | Buscador |
monthly_report.py | Reporte Excel cliente |
send_unbilled_week.py | Envío 837D |
- Solo seguro dental real (excluye self-pay / "Unknown Carrier")
- CDT válido (D + 4 dígitos). Excluye D9986/D9987/D9310/D9311
- Bandera timely filing
- Ante la duda enviar; 276 cuando el payer lo soporta; NO bulk masivo
Ficha 7 — Widget del dashboard (modular)
Archivo y modularidad
web/brandacare_recovery_widget.html lee el JSON por ?data= y prende/apaga tarjetas según d.modules (derivado del tier). Retrocompatible: si no viene modules, se infiere del tier.
Layouts
| Tier | Layout |
|---|---|
| full | Izq LIFETIME (total + gráfico A/R 6m); der THIS CYCLE (collected, A/R, verificaciones, no-shows) |
| verif-only (A+) | Tarjeta grande de billable (número azul) + "Total verified/horas/$"; a la derecha 2 tarjetas grises: Solstice ("complementary · on us") y No-shows ("no-show · no charge") |
- Fondo gris
#E2E6F0 - Reporta su altura por postMessage para que el iframe se autoajuste (sin scroll)
- Cache-buster en la URL:
?v=N(subir N en cada cambio de diseño) +&t=time()
Ficha 8 — Embed en WordPress (WPCode + Elementor)
Snippet PHP (WPCode)
web/wordpress_dashboard_snippet.php: shortcode [brandacare_dashboard]. Un array $clients mapea email de login → client_id, y muestra el iframe con el metrics_<id>.json que corresponde.
?client=<id> a la URL.
Cache-buster
El ?t=time() va en la URL del JSON (metrics_<id>.json?t=...), NO solo en el widget. Así la web nunca sirve datos viejos. Tras un cambio, purgar Breeze (Purge All Cache) y refrescar con Cmd+Shift+R.
Gris a todo el ancho
Wrapper con box-shadow + clip-path (full-bleed) y padding lateral para centrar/achicar el contenido. Alternativa nativa: sección Elementor con fondo #E2E6F0 y Content Width = Boxed.
Pasos de publicación en WordPress
1WPCode → editar el snippet → pegar la versión nueva → Update
2Si hay Breeze, Purge All Cache
3Refrescar con Cmd+Shift+R
4Página del cliente: shortcode [brandacare_dashboard]
Una sola página sirve para todos.
Ficha 9 — Deploy automático (GitHub Actions + Cloudways)
Componentes
| Archivo | Función |
|---|---|
run_pipeline.py | Lista CLIENTS=[...] y corre recovery_metrics por cada uno (alertas por email si algo falla) |
.github/workflows/daily.yml | Cron diario (08:00 UTC) + workflow_dispatch (botón manual). Restaura credenciales desde secrets, corre el pipeline, sube por SFTP (lftp) los metrics_*.json + el widget HTML a Cloudways |
Secrets (Settings → Secrets and variables → Actions)
| Secret | Contenido |
|---|---|
SERVICE_ACCOUNT_JSON | Google service account (para OPS Sheet) |
ENV_FILE | .env con keys generales |
CLIENT_HALLANDALE, CLIENT_BENITEZ, CLIENT_APLUS | JSONs de config por cliente |
SFTP_HOST/USER/PASS/PATH | Credenciales Cloudways |
.gitignore excluye llaves (clients/*.json, .env, service_account.json) y datos de pacientes (*.pdf, *.csv, BCBOT/, out/, screenshots). Nunca subir PHI. Usar git add con criterio (add -A agarra TODO).
Deploy manual (cuando cambia código o widget)
git add -A && git commit -m "..." && git push
Luego: GitHub → Actions → BrandaCare daily metrics → Run workflow → esperar el check verde (5-8 min).
repo + workflow (usuario ybrandacare, password = ghp_...).
Ficha 10 — Replicar a un cliente nuevo
1Crear clients/<id>.json
Con: open_dental (keys), stedi, provider, google_sheet (sheet_id, tab_prefix), tier, cycle_start_day, service_start_date, ops_doc_name, verification_value_per_usd (5.63).
Para verif-only: tier="verifications", noshow_source="unscheduled", tier_verifications_included, monthly_fee.
2Confirmar el nombre EXACTO de la columna "Doc" del OPS
Ponerlo en ops_doc_name.
3Compartir el OPS con el service account (lector)
claims-audit-bot@claims-audit-495602.iam.gserviceaccount.com
4Correr recovery_metrics con --debug
Validar 2-3 pagos/verificaciones contra Open Dental.
5Agregar el client_id a CLIENTS en run_pipeline.py
6Crear el secret CLIENT_<NOMBRE> en GitHub
Y restaurarlo en daily.yml (printf a clients/<id>.json).
7Agregar el email del cliente al array $clients
Del snippet WPCode.
8git push → Run workflow → verificar el dashboard
Con ?client=<id>.
Widget v1 (frozen, referencia)
El widget v1 congelado sigue vivo como referencia del layout original de Hallandale. Está en brand/widgets/brandacare_recovery_widget_v1.html.
web/brandacare_recovery_widget.html). El v1 se conserva como snapshot histórico.
