Skip to content

Enforce lead disqualification reason, group campaign/task fields, unify priority_rank - #577

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-575-a2-a3-a4-00yyrh
Jul 31, 2026
Merged

Enforce lead disqualification reason, group campaign/task fields, unify priority_rank#577
yinlianghui merged 1 commit into
mainfrom
claude/issue-575-a2-a3-a4-00yyrh

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Description

Items A2, A3 and A4 from #575 — the three A-group items scoped here. Nothing from the B group is touched. All three were re-verified against main (3ed27b26) before any code was written; all three still existed.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Related Issues

Related to #575

Deliberately not Fixes/Closes: this PR covers A2/A3/A4 only, and #575 still carries A1 and the whole B group.

Changes Made

A2 — crm_lead.disqualification_reason promised required and enforced nothing.
The field's own description has read "Required when status is Unqualified" since it was added, with no validation, no hook, and no form that rendered the field at all.

  • Adds the disqualification_reason_required script validation on crm_lead, modelled on the repo's existing crm_case.escalation_reason_required.
  • A rule with no writer is worse than no rule — a form that lets a user pick "Unqualified" but never shows the reason produces a save error the user cannot clear. The field is now on every crm_lead form that exposes an editable status (default form, quick_create, detail_form, lead_conversion_wizard, split_edit, quick_edit_drawer, status_update_modal, advanced_conditional), shown only when the status is unqualified, and on the lead detail page beside the status.
  • The four generated unqualified seed leads now carry a reason (rotated across four values, each with a matching note) instead of a budget-flavoured note and no reason.

A3 — crm_campaign and crm_task had no fieldGroups.
They were the last two business objects with a full detail page and zero groups, so both rendered as one flat grid — the campaign's ROI formulas inline with its name, the task's five polymorphic related_to_* lookups and recurrence machinery inline with its subject. Both now declare groups mirroring the sections their forms already use, with every field assigned. The two line-item objects stay ungrouped, as the issue allows: they are edited inline in the parent's grid and have no detail page to section.

A4 — priority_rank diverged between the two objects that use it.
The unknown-priority fallback was 1 on crm_case and 2 on crm_task, with field defaults to match — so the same unrecognised priority sorted differently on the two objects, and on each it was indistinguishable from a genuine priority (low / normal respectively). Both now use 0, an unranked sentinel that sorts below every real rank on the priority_rank desc queues.

  • The known ranks (1–4) are unchanged, so no seeded or stored row movestest/metadata-references.test.ts and test/hooks-runtime-service.test.ts, which pin this area, are untouched and still pass.
  • The two rank maps stay hand-copied on purpose: L2 hook bodies run body-only in the QuickJS sandbox, so a shared module constant resolves at authoring time and arrives as undefined (see _line-item-price-fill.ts). Since the duplication is forced, the new parity test is what keeps the copies honest — it drives both hooks and asserts they rank an unknown priority identically and consistently with each object's field default.

Testing

  • Unit tests pass (pnpm test) — 515 passed, 2 skipped, 28 files
  • Linting passes (pnpm lint)
  • Build succeeds (pnpm build)
  • New tests added

Full pnpm verify (validate → typecheck → lint → hygiene → build → test) is green. The two expression warnings the build emits are pre-existing (campaign_enrollment flow variable) and unrelated.

New guards live in three new test files, per the issue's convention, rather than being appended to test/metadata-references.test.ts:

  • test/lead-disqualification.test.ts — the rule exists and fires on exactly the right condition, the field still carries the promise the rule keeps, every form that can set status also offers the reason, and every seeded unqualified lead carries a legal reason value.
  • test/field-groups-coverage.test.ts — every detail-page object declares groups, plus the silent failures around them: a field pointing at an undeclared group key vanishes from the layout, and a declared group with no fields renders an empty section header.
  • test/priority-rank-parity.test.ts — both hooks rank the known priorities 1–4, fall back to the same sentinel for an unknown priority, and agree with their object's field default.

Checklist

  • I have added a changeset.changeset/lead-disqualification-field-groups-priority-rank.md
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes

Additional Notes

Per the issue's convention #5, pnpm verify should be re-run on main after merge — three "green at merge, red on main" incidents happened the day the issue was filed.


Generated by Claude Code

…fy priority_rank

Refs #575 (A2, A3, A4).

A2 — crm_lead.disqualification_reason promised "Required when status is
Unqualified" in its own description and nothing enforced it: no validation,
no hook, and no form that rendered the field. Adds the
disqualification_reason_required script validation (same shape as
crm_case.escalation_reason_required), puts the field on every crm_lead form
that exposes an editable status (shown only when the status is unqualified)
and on the lead detail page, and gives the generated unqualified seed leads a
reason with a matching note.

A3 — crm_campaign and crm_task were the last two detail-page business objects
with zero fieldGroups, so both rendered as one flat grid. Both now declare
groups mirroring the sections their forms already use, with every field
assigned. Line items stay ungrouped: they are edited inline in the parent's
grid and have no detail page to section.

A4 — priority_rank's unknown-priority fallback was 1 on crm_case and 2 on
crm_task, with field defaults to match, so the same unrecognised priority
sorted differently on the two objects and on each was indistinguishable from a
real priority. Both now use 0, an unranked sentinel below every real rank on
the priority_rank desc queues; the known ranks 1-4 are unchanged. The two rank
maps stay hand-copied because L2 hook bodies run body-only in the QuickJS
sandbox, so the new parity test is what keeps the copies in agreement.

New guards live in three new test files rather than being appended to
test/metadata-references.test.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSmk2xtzFcv9TbW9PuYBUe
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Jul 31, 2026 2:51pm

Request Review

@yinlianghui
yinlianghui marked this pull request as ready for review July 31, 2026 15:02
@yinlianghui
yinlianghui merged commit 28e87c9 into main Jul 31, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants