Retire dead lead/opportunity rules, stamp first response, guard CPQ lifecycles (#575 group B) - #578
Merged
Merged
Conversation
…ifecycles Group B of #575, following the plan agreed in the issue comments. B1 — delete `crm_lead`'s `cannot_edit_converted` validation. It was described as the friendly, recoverable half of a two-layer converted-lead lock, but the `beforeUpdate` throw in lead.hook.ts aborts the write first, so it never produced that error on any field. The hook is now the single guard. B2 — delete `crm_opportunity.created_date`, a duplicate of the platform's `created_at` with no writer: `deal_timeline` used it as `startDateField` and so had no start dates. The view reads `created_at`; the four locale packs no longer label a field that does not exist. `crm_case.created_date` is a different field with a real writer and is untouched. B2 — give `crm_case.first_response_date` the writer it never had, in the shared `logActivityAction` body: the first `sys_activity` on a case is when the customer first heard back (Salesforce `FirstResponseDateTime` / Zendesk first reply time), so a logged call or meeting is the event, not a status change. The field drops `readonly`, which would discard the write, and the body reads the stored value so a projected record cannot turn "first response" into "last". B4 — add `state_machine` validations to `crm_quote` and `crm_contract`, neither of which had a transition table or a hook-level status guard: `draft → accepted` and `draft → activated` were both legal. Warning severity, matching the three existing machines. `crm_campaign` and `crm_task` deliberately get none — their status is descriptive — and a test pins that absence. Guards land in four new test files rather than metadata-references.test.ts; the first-response tests run the shipped action body through the real QuickJS sandbox from #575 A1. Refs #575 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FvY1DtcReVbyfSh5PvzCE
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
July 31, 2026 15:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Group B of #575, executed to the plan agreed in the issue's two comments ("B 组方案已定" + "更正 B2 的实现方案"). B3 is out of scope —
.changeset/and the release flow are untouched apart from this PR's own changeset.Two of these remove rules that could never fire, one gives a shipped SLA metric its missing writer, and one constrains the two lifecycles that had no constraint at all.
Type of Change
Related Issues
Related to #575
Changes Made
B1 — delete
crm_lead'scannot_edit_convertedvalidation, keep the hook.The code described the pair as a deliberate division of labour: the validation for a friendly, recoverable error on the four identity fields, the hook for a hard stop on everything else. That division never existed at runtime — a
beforeUpdatethrow aborts the write before validations are evaluated, so the validation could not produce its friendlier error on any field, ever. Same shape as therevenue_positiverule deleted in #571. The hook is now the single guard, and the comments on both sides say so.B2a — delete
crm_opportunity.created_date.A duplicate of the platform's
created_atwith no writer anywhere — not the seed loader (which legitimately backdates readonly fields), not a hook, not a flow — so it was null on every row whiledeal_timelineused it asstartDateField, giving the timeline no start dates. The view now readscreated_at, the spelling the lead activity calendar already used; all four locale packs and the API reference drop the entry.crm_case.created_dateis a different field with a real writer and is untouched.B2b — give
crm_case.first_response_datea writer.It was the only member of the case SLA family without one (
sla_due_dateandresolution_time_hourscome fromcase.hook,is_sla_violatedfromcase_sla_monitor), so the most standard service-desk metric was permanently null while the field, its four translations and its place on the case detail page all advertised otherwise. It is now stamped in the sharedlogActivityActionbody on the firstsys_activitya case receives — the industry definition (SalesforceFirstResponseDateTime, Zendesk first reply time) is when the customer first heard back, so a logged call or meeting is the event and a status change deliberately is not. Two details are load-bearing:readonly, which would otherwise silently discard the write on a user-context write (#2948) — the same reasonis_sla_violatedandescalated_dateare not readonly;ctx.record, becauselist_item/record_relateddispatch can hand it a projected record, and a missing key there would re-stamp on every log and turn "first response" into "last response".A convention note in the body records that any future customer-facing path on a case (a reply-email action, an inbound portal reply) must stamp it too.
B4 —
state_machinevalidations forcrm_quoteandcrm_contractonly.Neither had a transition table or a status guard in its hook, so
draft → acceptedon a quote (binding numbers nobody reviewed or sent) anddraft → activatedon a contract (which stampssigned_date, promotes the account tocustomer, and starts the renewal clock) were both legal. Warning severity, matching the lead / opportunity / case machines. The tables agree with the automation that drives them: every unsettled quote status can still expire (the sweep filters onexpiration_datealone),accepted/expiredare terminal becausequote_pricing_guardfreezes them, and only an activated contract expires.crm_campaignandcrm_taskdeliberately get nothing — their status is descriptive, not a controlled lifecycle — and a test pins that absence so it stays a decision rather than a gap.Testing
pnpm test) — 33 files, 630 passed / 2 skippedpnpm lint)pnpm build)pnpm verifygreen locally (validate → typecheck → lint → hygiene → build → test)Four new test files, per the issue's convention of not appending to
test/metadata-references.test.ts:test/converted-lead-guard.test.tstest/case-first-response.test.ts(data, options)update shape; the stored-value read; the field is notreadonlytest/opportunity-creation-date.test.tscrm_case.created_dateintact, no locale still labels it, no opportunity view anchors on a date field that does not existtest/status-state-machines.test.tsThe first-response tests run the shipped action body through the real QuickJS sandbox added in #575 A1, because the two things that can break this stamp only exist there: the
api.readcapability the new lookup needs, and the engine facade'supdate(data, options)signature — the onemass_update_stagegets wrong and silently never writes.Checklist
.changeset/lifecycle-guards-and-dead-fields.mddocs/developers/api_reference.md)Additional Notes
Per the issue's conventions: the problems were re-verified against
mainat28e87c9(after #576 and #577 landed) before starting, and the commit message usesRefs #575rather thanCloses, since group B is only part of the issue.Worth knowing for review:
test/hooks-runtime-service.test.tsexercisescontract_on_activationwith adraft → activatedfixture, which the new contract table now flags as a skipped-approval jump. The fixture is untouched — hooks bypass validations, so nothing breaks, and the hook's behaviour genuinely does not depend on the origin state — but it is the one place in the repo where the new rule and an existing test scenario disagree on intent.Generated by Claude Code