ES
SOP-IT-003 · v3.0
Updated Jul 5, 2026

BrandaCare Metrics & Dashboards

Multi-client system over Open Dental (API) + OPS Google Sheet + Stedi

Purpose

The system generates, per billing cycle and per client, a WordPress-embedded dashboard (brandacare.com) with: insurance money collected, A/R recovered, insurance verifications, and no-shows. The widget is ONE and modular: turns cards on/off based on the client's "tier". Everything runs itself, daily, via GitHub Actions, and publishes via SFTP to Cloudways.

Restricted access: contains OD, Stedi, SFTP and GitHub keys. Do not share with operations team.

Section 1 — Architecture, tiers, and client roster

General flow

Open Dental (API) + OPS Google Sheet (tab "ALL DATA", filtered by "Doc")
                              ▼
                     recovery_metrics.py
                              ▼
                output/metrics_<id>.json
                              ▼
        widget in /dashboards/ on Cloudways
                              ▼
         iframe in WordPress (email-gated)

run_pipeline.py orchestrates all clients; GitHub Actions runs it daily and uploads the JSONs + widget via SFTP.

Tiers (define which cards the client sees)

TierCardsExamples
billing+insurance (full)Total recovered lifetime + A/R 6m chart + Insurance collected + A/R recovered + Verifications + No-showsHallandale, Casas
billing+arTotal recovered lifetime + A/R chart + Insurance collected + A/R recovered. NO verificationsBenitez
verifications (verif-only)ONLY verifications + Solstice + No-shows. Hides all money numbers, even if OD has them. Skips payment extraction → runs fastA+

Client roster (jun 2026)

ClientTier / SourceDashboard statusNotes
Hallandale Dental Carebilling+insurance (OD API)LIVEMVP
Benitez Dental Centerbilling+ar (OD API)LIVELifetime A/R $13,192
A+ Dental of Aventuraverifications (OD API)LIVENo-show via Unscheduled List
Dr Casas Family Dentistrybilling+insurance (OD API)PENDINGWaiting for more A/R to reflect work
G-DentalDentrix (no API)FUTURECross-match OPS + Dentrix reports
Le DentisteDentrix (no API)FUTURECross-match OPS + Dentrix reports

Section 2 — Money metrics (recovery_metrics.py)

The 3 figures (exact definitions)

FigureDefinition
TOTAL RECOVERED (lifetime)Accumulated A/R recovered since service_start_date.
INSURANCE COLLECTED (cycle)ALL insurance collected in cycle, regardless of age.
A/R RECOVERED (cycle)Subset collected with DOS of 45+ days — old claims that required follow-up.

"Recovered" rule (crystal clear)

  • A/R recovered = payment posted MORE than 45 days after DOS (CheckDate − ProcDate > 45).
  • The date floor is on PAYMENT DATE (>= service_start_date), NOT on the DOS. A claim with old DOS (even Dentemax era) collected during our engagement DOES count.
  • Code by code: each claimproc counts literally. NO deduplication by amount.

Sources and filter

New flags

FlagFunction
Verif-only (tier=verifications)SKIPS od_insurance_payments (do_billing=False) → much faster and shows no money.
--as-of YYYY-MM-DDCuts the count to that date (day snapshot, e.g. to compare with a manual report).

Command and outputs

python recovery_metrics.py --client <id> \
    --ops-sheet <SHEET_ID> --ops-tab "ALL DATA" \
    [--as-of 2026-06-28] --debug

Section 3 — Insurance verifications

What counts

Value (ADA reference — updated)

MetricValueNote
Time saved per verification10 minutesMINUTES_SAVED_PER_VERIFICATION = 10, same as the web
Money saved per verification$5.63ADA. Implies ~$33.78/hour — higher than raw salary ($20-22) because it captures errors/denials avoided
Client-editableverification_value_per_usdClient config

Solstice (courtesy) — NEW

Key rule:
  • Identified by carrier: the verifications detail now brings "Ins Carrier" (from OPS) and a "Solstice?" flag.
  • solstice_count = verifications whose carrier contains "solstice".
  • Carrier is crossed by patient from OPS; may be unknown for verifications only from OD notes (no OPS match), so solstice_count is a floor.
Never use "free": Solstice and no-shows are shown as "complementary · on us" / "no charge".

Section 4 — No-shows (two definitions per client)

Default (broken on the calendar)

A+ and similar: noshow_source = "unscheduled" — NEW (key)

Problem solved: A+ doesn't mark "Broken": sends the appointment to Unscheduled List (AptStatus=UnschedList), which has no calendar date, so day-by-day scanning returned 0.

Cut to TODAY (don't count the future) — jul-2026 fix

Critical fix:
  • In an ONGOING cycle we count only up to today: ce_eff = min(end_of_cycle, today).
  • Before it took the whole cycle (until the end), and grabbed Unscheduled appointments with future AptDateTime (planned treatments not yet occurred) as if they were no-shows.
  • False positive signature: DateTStamp BEFORE AptDateTime (appointment edited before its date = future). A real no-show is marked on/after its appointment day.
  • The --as-of flag uses the same logic to reproduce a past day snapshot.

Section 5 — A+ Model (verifications-only) — NEW

A+ pays ONLY for verifications (no billing or A/R). The dashboard shows the billable number clearly and adds courtesies to inflate the total value, validating the price.

CategoryFormula
Billable (what they pay)Completed verifications − Solstice
Courtesy (no charge)Solstice + No-shows (verified, didn't come)
Total verifiedCompleted + no-shows

Value math (hours and $) goes over the TOTAL.

Example (Jun 2026):
294 billable + 30 Solstice + 137 no-show = 461 total verified76.8 hrs$2,595 (vs ~$1,000/month they pay → 2.4x)

JSON fields

billable_verifications, solstice_count, broken_appointments,
total_verified, verification_value_total_usd, hours_saved_total

Section 6 — A/R Recovery (unbilled procedures)

Finds completed procedures WITHOUT a claim in Open Dental (post-Dentemax), identifies billable ones and allows sending to insurance.

ScriptFunction
find_unbilled_procedures.pyFinder
monthly_report.pyClient Excel report
send_unbilled_week.py837D submission

Section 7 — Dashboard widget (modular)

File and modularity

ONE template for all: web/brandacare_recovery_widget.html reads the JSON via ?data= and turns cards on/off based on d.modules (derived from tier). Backward compatible: if modules is missing, it's inferred from tier.

Layouts

TierLayout
fullLeft LIFETIME (total + 6m A/R chart); right THIS CYCLE (collected, A/R, verifications, no-shows)
verif-only (A+)Big billable card (blue number) + "Total verified/hours/$"; on the right 2 gray cards: Solstice ("complementary · on us") and No-shows ("no-show · no charge")

Section 8 — WordPress embed (WPCode + Elementor)

PHP snippet (WPCode)

web/wordpress_dashboard_snippet.php: shortcode [brandacare_dashboard]. A $clients array maps login email → client_id, and shows the iframe with the correct metrics_<id>.json.

Add a client: one new line in the array. Admin can preview any with ?client=<id> in the URL.

Cache-buster

The ?t=time() goes in the JSON URL (metrics_<id>.json?t=...), NOT just in the widget. That way the web never serves old data. After a change, purge Breeze (Purge All Cache) and refresh with Cmd+Shift+R.

Full-width gray

Wrapper with box-shadow + clip-path (full-bleed) and lateral padding to center/shrink content. Native alternative: Elementor section with #E2E6F0 background and Content Width = Boxed.

Publish steps in WordPress

1WPCode → edit snippet → paste new version → Update

2If Breeze installed, Purge All Cache

3Refresh with Cmd+Shift+R

4Client page: shortcode [brandacare_dashboard]

One page works for everyone.

Section 9 — Auto deploy (GitHub Actions + Cloudways)

Components

FileFunction
run_pipeline.pyLists CLIENTS=[...] and runs recovery_metrics for each (email alerts if something fails)
.github/workflows/daily.ymlDaily cron (08:00 UTC) + workflow_dispatch (manual button). Restores credentials from secrets, runs the pipeline, uploads via SFTP (lftp) the metrics_*.json + widget HTML to Cloudways

Secrets (Settings → Secrets and variables → Actions)

SecretContent
SERVICE_ACCOUNT_JSONGoogle service account (for OPS Sheet)
ENV_FILE.env with general keys
CLIENT_HALLANDALE, CLIENT_BENITEZ, CLIENT_APLUSPer-client config JSONs
SFTP_HOST/USER/PASS/PATHCloudways credentials
Repo hygiene (PRIVATE): .gitignore excludes keys (clients/*.json, .env, service_account.json) and patient data (*.pdf, *.csv, BCBOT/, out/, screenshots). Never push PHI. Use git add carefully (add -A grabs EVERYTHING).

Manual deploy (when code or widget changes)

git add -A && git commit -m "..." && git push

Then: GitHub → Actions → BrandaCare daily metrics → Run workflow → wait for green check (5-8 min).

Git auth: token (PAT classic) with repo + workflow scope (user ybrandacare, password = ghp_...).

Section 10 — Replicate to a new client

1Create clients/<id>.json

With: 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).

For verif-only: tier="verifications", noshow_source="unscheduled", tier_verifications_included, monthly_fee.

2Confirm EXACT name of OPS "Doc" column

Put it in ops_doc_name.

3Share OPS with service account (reader)

claims-audit-bot@claims-audit-495602.iam.gserviceaccount.com

4Run recovery_metrics with --debug

Validate 2-3 payments/verifications against Open Dental.

5Add client_id to CLIENTS in run_pipeline.py

6Create the CLIENT_<NAME> secret in GitHub

And restore it in daily.yml (printf to clients/<id>.json).

7Add client's email to $clients array

In the WPCode snippet.

8git push → Run workflow → verify the dashboard

With ?client=<id>.

Widget v1 (frozen, reference)

The frozen v1 widget lives on as reference for Hallandale's original layout. It's at brand/widgets/brandacare_recovery_widget_v1.html.

Note: v1 is NOT what runs in production — the current one is modular (web/brandacare_recovery_widget.html). v1 is kept as a historical snapshot.