On this page
The one test that gates the releaseEnvironment reality checkTest matrix — the churn changeTest matrix — the disabled-companies changeEdge cases worth explicit testsThe ordering defect — how to reproduce itNon-transactional moveWhat does not need testingRelated readingFor Quality
The one test that gates the release
Everything else on this page is secondary to this check.
Do the Churned and Renewal stages share a LeadView?
The move is guarded by companyLeadCron.js:82:
patient.lead.stage.viewId !== renewalStage.viewId
This compares views, not stages. So a churned lead only moves if its stage's viewId differs from the Renewal stage's viewId.
Run against the production (or a production-shaped) database:
SELECT id, name, "viewId"
FROM "LeadStage"
WHERE id IN (
'9dbf0d3f-ad02-45b8-8a3c-4b605435dde6', -- RENEWAL_STAGE_ID
'2397d5d6-0ecd-4796-9f8e-aa78db0a1a79' -- the removed CHURNED_STAGE_ID
);
| Result | Verdict |
|---|---|
Different non-null viewIds | The PR does what its title says. Proceed. |
Same viewId | Churned leads are scored and annotated but never move. The headline behaviour does not ship. Raise it. |
Both viewId are NULL | Same as above — null !== null is false, so the guard fails and nothing moves. |
| Renewal row missing | The job crashes. See the null-dereference test below. |
Note the second UUID no longer appears anywhere in the codebase — it is recorded here only because this PR deleted it. Confirm the real Churned stage ID with the team rather than assuming this literal is still correct.
Environment reality check
The branch sandbox cannot exercise this feature. Document this before anyone reports a false pass.
We ran the job manually via Admin → Jobs on the sandbox. It completed in 7 ms with Total = 2 and no error — but it did no work at all, because:
- The job only processes companies with a truthy
referenceId(companyLeadCron.js:43).
Both seeded sandbox companies have an empty Reference ID — visible on the company detail screen in the Feature Tour.
- Even if one had a
referenceId, the patient lookup at:47is scoped to the hardcoded
MAIN_COMPANY_ID, which does not exist in the sandbox seed.
- And
RENEWAL_STAGE_IDdoes not exist either, sorenewalStageisnull.
A 7 ms clean run is therefore the signature of a complete no-op, not a pass. Any test plan that treats "the job ran without error" as evidence is testing nothing.
To test this properly you need a seeded fixture with: a main company at the hardcoded UUID, a Renewal stage and a Churned stage at known UUIDs in known views, a company with a referenceId, and a patient in the main company with a matching referenceId and an attached lead.
Also note: the admin Edit Company form is broken in this build — submitting returns Variable "$referral" of required type "ID!" was not provided because the seeded company has no referral. That blocks setting referenceId or leadsEnabled through the UI. It is a pre-existing issue unrelated to this PR, but it will block your fixture setup.
Test matrix — the churn change
shouldMoveToRenewal requires all four of: different view, !monthly, truthy tierExpiry, expiry inside the window.
| # | Lead stage | Plan | tierExpiry | Expected |
|---|---|---|---|---|
| 1 | Churned, different view from Renewal | annual | inside window | Moves to Renewal. The headline case. |
| 2 | Churned, same view as Renewal | annual | inside window | Scored, tagged, noted. Does not move. |
| 3 | Churned | monthly | inside window | Scored and noted. Does not move. |
| 4 | Churned | annual | outside window | Scored and noted. Does not move. |
| 5 | Churned | annual | already past | Depends only on the window test — a past date is "before" the cutoff, so it does move. Confirm this is intended. |
| 6 | Already in Renewal | annual | inside window | Scored and noted. Does not move (same view as itself). |
| 7 | Any other stage | annual | inside window | Moves. Unchanged from before the PR. |
Case 5 deserves attention. The condition is isBefore(cutoff) with no lower bound, so a subscription that expired two years ago satisfies it. Combined with the churn change, every long-dead churned account with an annual plan becomes eligible to move to Renewal on the first run. If the churned population is large, the first run could dump a substantial batch into Renewal. Verify whether that is intended; if not, it needs a lower bound.
Regression coverage — the pre-PR behaviour that must still hold
| Scenario | Expected |
|---|---|
| Non-churned lead, all conditions met | Still moves. |
Company with no referenceId | Skipped entirely. |
Company with referenceId but no matching patient | Skipped. |
| Patient exists but has no lead | Manager name/phone still synced (:65-78 is above the guard); nothing else runs. |
| Disabled company | Not fetched at all. |
Test matrix — the disabled-companies change
The processed set must be identical before and after. Company.disabled is Boolean @default(false) (schema.prisma:125), non-nullable, so where: { disabled: false } and !company.disabled select the same rows.
| Check | Expected |
|---|---|
Disabled company with referenceId and a live lead | Not processed. No score change, no note, no move. |
Total on the Jobs page | Equals the count of non-disabled companies. Verified on sandbox: Total = 2 for 2 non-disabled companies. |
| Enabled company | Processed exactly as before. |
The only behavioural delta is the total statistic. Confirm it dropped and that nobody reads it as a failure.
Edge cases worth explicit tests
Null Renewal stage. renewalStage is fetched with findUnique (:37-40) and dereferenced at :82 without a guard. Delete or rename the Renewal stage row and run the job: it should throw TypeError: Cannot read properties of null on the first company with a linked lead, and the catch at :295 should record it in the Error column. Confirm the error surfaces on the Jobs page rather than vanishing.
Mid-run failure. Force an exception partway through the company loop. Expect: every company after the failure point is skipped for that cycle, and the company being processed is left half-written (counters moved, card not). Because ordering is tierExpiry: 'desc', the most urgent renewals are processed last and are the first casualties.
Stuck lock. Kill the cron process mid-run, then wait for the next scheduled tick. Expect the job to never run again — the redis key has no TTL and there is no finally block. Only a cron-process restart clears it (cron.index.js deletes all job keys on boot). Confirm this behaviour so it is documented rather than discovered.
Score boundaries. The tag is chosen from the raw 0–17 count while the stored score is capped at 10 (:233, :242).
| Raw score | Stored | Tag |
|---|---|---|
| 0 | 0 | Not Working |
| 1 | 1 | Working Low |
| 10 | 10 | Working Low |
| 11 | 10 | Working |
| 17 | 10 | Working |
Assert the 10→11 boundary specifically: both display 10 / 10 but carry different tags.
Tag preservation. A lead with manually applied tags plus an automation tag must keep the manual ones and have only the automation tag replaced (:239-244).
Five-day window overlap. The job runs every 3 days but looks back 5. A single event should be counted in two consecutive runs without side effects, and a clinic going silent should take two runs to reach Not Working.
Renewal window boundaries. The cutoff is dayjs().add(26, 'days').subtract(1, 'seconds').endOf('month'). Test on the 1st, the 5th and the 28th of a month — the effective lead time ranges from ~26 to ~57 days. Specifically test a run where now + 26d lands exactly at midnight on the 1st; the subtract(1, 'seconds') exists to keep that from rounding up into the following month.
The ordering defect — how to reproduce it
This is pre-existing but this PR makes it fire far more often, so it belongs in the test plan.
At :207-212:
where: { stageId: patient.lead.stageId, order: { gte: patient.lead.order } }
order is not in the lead's select list (:49-62), so patient.lead.order is undefined. Prisma drops undefined filter keys, leaving { stageId } — so every lead in the source stage is decremented.
Repro: put five leads in a stage with orders 1–5. Trigger an automated move of the lead at order 3. Expected-correct result: remaining orders 1, 2, 3, 4. Actual: 0, 1, 2, 3 — the two cards above the moved one were shifted when they should not have been, and order 0 now exists.
Run it repeatedly and orders drift negative; Lead.order is a plain Int (schema.prisma:5183) with no constraint. Compare against the interactive path, moveLead.js:183-186, which uses gt and a correctly-loaded order.
Because Churned is now a source stage and is typically the largest column, one churned move rewrites every churned card's position.
Non-transactional move
:192-247 performs six writes with no prisma.$transaction, unlike moveLead.js:129. Inject a failure after the totalLeads increment (:193) but before the lead.update (:230) and confirm the counter is left inflated with no matching card, and that nothing repairs it.
This matters beyond a wrong badge: LeadStage.totalLeads is the allocator for the next card's order in the create paths (addNewLead.js:40-41, 93), so a desynced counter produces duplicate order values on later inserts.
What does not need testing
- Any clinic-facing screen or permission — no customer-facing code path is touched.
- The manager name/phone sync (
:65-78) — above the changed guard, unaffected. - The seventeen probes themselves — unchanged by this PR, though they now run for more
companies.
- GraphQL schema, permissions, migrations — none changed.
Related reading
- Stage Move Mechanics and Known Defects — full defect analysis
- Feature Tour — the sandbox evidence referenced above
- Change 1 — Churned Leads Rejoin Renewal