Dentolize · Churned Leads Rejoin Renewal — Walkthrough
On this page1. The admin cron dashboard — before2. The same dashboard after a manual run — the evidence for Change 23. The companies list — where subscriptions live4. A company record — every field the job touches5. Why the sandbox run was a no-op — read this before trusting any test6. Not Working Companies — the manual version of the same question7. The clinic side — what the seventeen signals actually measureWhat could not be captured

Feature Tour (screenshots)

Every image below was captured from the branch sandbox at https://mo-update-lead-cron.sandbox.anastawfik.com running commit 5ea0c6e.

Read this first. This PR has no user interface. It changes a scheduled backend job. So this tour does not show a new feature — it shows the screens where the job's inputs and outputs live, and one screen that provides direct evidence of the behaviour change.

It also honestly shows the limits of what the sandbox can demonstrate: the job is effectively a no-op here, and the last section explains exactly why.


1. The admin cron dashboard — before

Admin → Jobs, /ZROYKuKEVCvQykPlS4kP/jobs (packages/clinic-web/src/containers/AdminDashboardRouter.js:143)

The admin cron jobs dashboard listing all nine scheduled jobs. companyLeadCron is the last row, with empty Started, Duration and Total columns because it has not run in this environment.
The admin cron jobs dashboard listing all nine scheduled jobs. companyLeadCron is the last row, with empty Started, Duration and Total columns because it has not run in this environment.

All nine scheduled jobs, with companyLeadCron last. The columns — Running, Started, Duration, Total, Error, Execute — come from packages/clinic-web/src/components/admin/cronJobs/CronJobsStats.js:32-64, and the data is the redis payload each job writes about itself, read back through the getCronJobs query (packages/server/src/resolvers/queries/adminQueries.js:461-469).

Every row is blank because no job has run in this sandbox yet. This is the screen support and operations use to answer "did the renewal automation run?".


2. The same dashboard after a manual run — the evidence for Change 2

We triggered companyLeadCron with the Execute button, which calls the executeCronJob mutation (packages/server/src/resolvers/mutations/actions/cronJobs/executeCronJob.js:35, admin-only and hard-blocked in production).

The same dashboard after running companyLeadCron. Its row now shows Started 'a few seconds ago', Duration 7 ms, Total 2, and no error.
The same dashboard after running companyLeadCron. Its row now shows Started 'a few seconds ago', Duration 7 ms, Total 2, and no error.

The companyLeadCron row now reads Started: a few seconds ago · Duration: 7 ms · Total: 2 · no error.

Total = 2 is the observable effect of Change 2. That figure is companies.length from companyLeadCron.js:289 — the number of company rows the job fetched. After this PR the fetch carries where: { disabled: false }, so it counts only non-disabled companies. There are exactly two non-disabled companies in this sandbox (next screenshot), and the number matches.

Before the PR this figure would have included disabled companies too. On production it will drop. That is a definition change, not a regression — see Change 2.

The 7 ms duration is the honest part of this screenshot, and section 5 explains what it really means.


3. The companies list — where subscriptions live

Admin → Companies, /ZROYKuKEVCvQykPlS4kP

The admin Companies table showing two companies, Sandbox Dental and Sandbox Clinic, each with a Tier Expiry of 'in a year', a subscription price, and Renew Subscription and Add Payment buttons.
The admin Companies table showing two companies, Sandbox Dental and Sandbox Clinic, each with a Tier Expiry of 'in a year', a subscription price, and Renew Subscription and Add Payment buttons.

Two companies, both non-disabled — matching the Total = 2 above.

The Tier Expiry column (Companies.js:136-143, rendered red when under 50 days out) is the field the renewal test reads. Both sandbox companies expire "in a year", so neither is anywhere near the renewal window.

This table also carries the Monthly column (Companies.js:322-323) and an Unused Features column with a filter (Companies.js:527-533) — the latter being the field the job writes on every run, and which was silently stale for every churned account until this PR.


4. A company record — every field the job touches

Admin → Companies → Sandbox Dental, /ZROYKuKEVCvQykPlS4kP/company/:id

The Sandbox Dental company detail page. Reference ID shows only a '#' placeholder and is empty. Tier Expiry reads 'in a year'. Account Manager Name, Account Manager Phone and Unused Features are all blank.
The Sandbox Dental company detail page. Reference ID shows only a '#' placeholder and is empty. Tier Expiry reads 'in a year'. Account Manager Name, Account Manager Phone and Unused Features are all blank.

This single screen shows the job's entire read/write surface on the company record:

Field on screenRole in the jobCode
Reference IDThe join key to the CRM card. Falsy ⇒ company skipped entirely.:43, :48
Tier ExpiryDrives the renewal window test; also copied onto the lead's due date.:84-90, :238
Account Manager Name / PhoneWritten by the job from the CRM card's doctor.:71-77
Unused FeaturesWritten every run — codes 1–17 for each unused module.:163-186

Note that Reference ID is empty (it renders as a bare #), and Account Manager and Unused Features are blank. That is the whole explanation for section 5.

Monthly — the annual-versus-monthly flag that gates the renewal move (:83) — is on the Edit Company form (CompanyForm.js:335) rather than this summary.


5. Why the sandbox run was a no-op — read this before trusting any test

The 7 ms clean run in section 2 did no work at all. Three independent reasons:

  1. Neither company has a referenceId (visible in section 4). The guard at

companyLeadCron.js:43 skips them before anything else happens.

  1. The patient lookup at :47-48 is scoped to the hardcoded MAIN_COMPANY_ID

(54176b70-…), which does not exist in this sandbox seed.

  1. RENEWAL_STAGE_ID (9dbf0d3f-…) does not exist either, so the findUnique at :37-40

returns null.

Reason 3 is the interesting one: renewalStage.viewId is dereferenced at :82 with no null guard. The only thing preventing a TypeError here is that reasons 1 and 2 stop execution before it. A sandbox that had a referenceId but no Renewal stage would crash.

A fast, clean run is the signature of a complete no-op, not a pass. Any test plan that treats "it ran without error" as evidence is testing nothing. See For Quality for what a real fixture needs.


6. Not Working Companies — the manual version of the same question

Admin → Not Working Companies, /ZROYKuKEVCvQykPlS4kP/notWorking

The Not Working Companies screen, filtered to companies that didn't create Patients for 2 days, listing Sandbox Clinic.
The Not Working Companies screen, filtered to companies that didn't create Patients for 2 days, listing Sandbox Clinic.

A manual, ad-hoc version of the question the job answers automatically: which companies haven't created a given record type for N days? Here it is set to Patients over 2 days.

The job generalises this to seventeen record types over a fixed five days, and instead of producing a list on demand, it writes the answer onto each customer's card as a score, a tag, and a written note. The churned population is now included in that — which it was not before.


7. The clinic side — what the seventeen signals actually measure

Clinic app, logged in as owner

The Dentolize clinic dashboard for Sandbox Dental, with the left navigation showing Calendar, Patients, Reminders, Lab Orders, Finances, Inventory, Associates, Settings, Logs and Analytics.
The Dentolize clinic dashboard for Sandbox Dental, with the left navigation showing Calendar, Patients, Reminders, Lab Orders, Finances, Inventory, Associates, Settings, Logs and Analytics.

The customer-facing product, unchanged by this PR and shown for context only. The navigation items map closely onto the seventeen activity probes at companyLeadCron.js:92-142 — patients, appointments, lab orders, finances (invoices, payments, expenses, transactions), inventory, reminders.

When the automation says a clinic scored 4 out of 17, this is the surface where those four things happened.


What could not be captured

The lead board itself — the internal sales pipeline where cards move between Churned and Renewal — is not reachable in this sandbox.

It lives at /leads and /leads/journey/:leadView (packages/clinic-web/src/containers/DashboardRouter.js:892-893) but is gated on user.company.leadsEnabled (Sidebar.js:284), which is Boolean @default(false) (schema.prisma:135) and off for both seeded companies. There is no Leads item in the sidebar in section 7 for that reason.

We attempted to enable it through Admin → Edit Company, and the mutation was rejected with Variable "$referral" of required type "ID!" was not provided — the admin edit form cannot submit for a company with no referral. That is a pre-existing bug in this build, unrelated to this PR, but it blocks the fixture. The sandbox was left unchanged as a result.

Even with the flag on, the board would be empty: the cards this job manipulates live in Dentolize's own tenant (MAIN_COMPANY_ID), which is not part of the sandbox seed.

So the screens where the change is ultimately visible to a human — a card carrying a Working Low tag and a ✅/🔴 scorecard note, sitting in the Renewal column — cannot be shown from this environment. What a card displays is documented instead in Usage Scoring, Tags and Activity Notes, sourced from packages/clinic-web/src/components/dashboard/leads/LeadsBoard/LeadsCard.js:44-86.