Dentolize · Churned Leads Rejoin Renewal — Walkthrough
On this pageDecision summaryWhy this matters commerciallyWhat we are confident about, and what we are notRisk assessmentWhat to watch after deployFollow-up work this surfacesRelated reading

For Stakeholders

Decision summary

What: A four-line change to an internal scheduled job so that customers marked "churned" are re-included in Dentolize's own renewal and customer-health automation.

Customer impact: None. Backend only, internal tooling only, no clinic-facing surface.

Revenue relevance: Potentially real. Churned-but-still-contracted customers were invisible to the renewal process. This makes them visible.

Risk: Low in blast radius, moderate in one specific respect — it routes a new and possibly large population of records through a code path with three known pre-existing defects.

Recommendation: Ship, but confirm one configuration fact first (below) and watch two metrics after.


Why this matters commercially

Dentolize tracks its own customers on an internal board. "Churned" is a column somebody drags a card into — a human judgement, recorded at a point in time. It is not derived from billing and it is not automatically reconciled against it.

That creates a structural gap. A customer marked churned in March whose annual subscription runs to November is, for eight months, simultaneously "lost" on the board and "paying" in the ledger. Until this change, the automation skipped that card entirely, so:

  • no renewal prompt was ever generated for them,
  • their usage score froze, so a customer who quietly resumed using the product produced no

signal,

  • and a card moved to Churned by mistake stayed invisible permanently — there was no automated

route back out.

The size of the opportunity is exactly "how many churned accounts still hold unexpired annual contracts". Nobody has measured that. That number is worth pulling before the deploy — both to size the benefit and to predict the volume landing on the sales team.


What we are confident about, and what we are not

Being precise here matters, because the PR title promises more than the code unambiguously delivers.

Confirmed by reading the code

Churned cards now re-enter the full scoring pipeline. They will get, every three days:

  • a refreshed usage score across seventeen activity signals,
  • a refreshed health tag,
  • a refreshed unusedFeatures list on the account record — which drives an admin filter that

has been silently stale for every churned account,

  • and a written scorecard note.

This is unconditional and will definitely happen.

Not confirmed

Whether churned cards actually move to Renewal. The automation declines to move a card that already sits in the same pipeline as the Renewal column. Whether the Churned and Renewal columns share a pipeline on the production board is a data fact, not a code fact, and nothing in the repository settles it.

  • If they are in different pipelines → the headline behaviour works.
  • If they share one → churned cards get scored but never move, and the PR does not do what its

title says.

This is a five-minute check against production data. It should be done before ship, and the outcome should be recorded, because the entire commercial case for the "renewal" half of this change depends on it. See For Quality.


Risk assessment

RiskSeverityNotes
Customer-facing regressionNoneNo clinic-facing code path is touched.
Wrong accounts moved to RenewalLowGuards on annual-plan and expiry-window are unchanged and independently correct.
Ordering corruption in the Churned columnMediumPre-existing defect, newly exposed. See below.
Column counter driftLow–MediumPre-existing. No transaction around the move; no reconciliation job exists.
Increased job runtimeLowMore companies now reach the 17-probe block. See below.
Sales team overload from a first-run batchLowPredictable and one-off, but worth pre-warning the team.

The ordering defect, stated plainly

The move code decrements the board position of every card in the source column rather than only those below the departing card, because the position field is never loaded from the database. This is a pre-existing bug — it has been there as long as the job.

This PR does not introduce it, but it does make Churned a source column for the first time, and Churned is typically the largest column on the board. So the practical frequency of the bug goes up materially.

The consequence is cosmetic — manual card ordering in the Churned column degrades — and it is not data loss. But it should be logged as follow-up work rather than discovered in production. Details: Stage Move Mechanics and Known Defects.

Runtime

Each company that reaches the scoring block costs seventeen sequential database queries plus up to seven writes. Churned companies previously cost zero of those. Runtime therefore scales with the churned population.

There is no batching, no concurrency, and no timeout. The whole run is one try/catch, so a single failure aborts everything after it — and because companies are processed latest-expiry-first, the accounts with the most urgent renewals are processed last and are the first to be dropped by a mid-run failure. That inversion is pre-existing but is worth knowing when judging the cost of a failed run.

An operational hazard worth naming

The job's concurrency lock has no expiry. If the process dies mid-run, the lock stays set and the job silently never runs again until the cron process restarts. No error, no alert. This is true of all nine scheduled jobs, not just this one, and is unaffected by this PR — but as this job becomes more load-bearing commercially, that failure mode becomes more expensive. Worth a separate ticket.


What to watch after deploy

  1. Volume into Renewal in the first two runs. If it is zero, the pipeline-configuration

question above was answered "same view" and the headline feature is not live.

  1. Job duration and error state on the admin Jobs page. Runtime will rise; a persistent

error or a stuck "running" state is the thing to catch.

  1. Column counters versus actual card counts, spot-checked once after the first few runs.
  2. The Total figure will drop, because it now counts only non-disabled companies. This is

an intended definition change, not a regression — do not let it be read as one.


Follow-up work this surfaces

Not blockers, but they should be tickets:

  • Load order in the lead select and change gte to gt — fixes the ordering defect.
  • Wrap the stage move in a transaction, matching the interactive path.
  • Add a TTL to the redis job locks across all nine jobs.
  • Add a null guard on the Renewal stage lookup — a missing stage row currently crashes the run.
  • CHURNED_STAGE_ID is now dead code repository-wide, and the word "churned" appears nowhere

in the codebase. If churn needs special handling again, it will have to be rebuilt.