Repository navigation
fix(guards): the account and contact deletion guards name what really lets the delete through - #2027
Merged
Merged
Conversation
…tract (#2019) account_protection counted open opportunities only and told the user to "Close or reassign it first"; closing the deal was answered by the engine's DELETE_RESTRICTED on crm_opportunity all the same, because the required crm_account lookup restricts on any deal. A closed deal is the account's sales history, so the guard now counts every opportunity and its sentence names what really frees the account (delete the deal) and the retirement path that keeps the history (mark the account inactive). contact_integrity told the user to "Close or reassign those records first"; terminating or expiring a contract left the delete refused on the required crm_contract.crm_contact. A contract keeps its primary contact for its whole life, so the sentence now says to give each contract another primary contact. The count stays activated: the guard also runs inside an account delete, which takes ended contracts with it (#549). Flips the two measured-defect pins to the business facts, follows the guard sentences in the end-to-end pins, and fixes the account deletion rule in the three-locale accounts page. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
…ured ~57k On the merge with main (f7cfb65) the token gate's Headline reads business semantics ~57k (56,746 tokens), above the ~54k banner's 5% band (51,300-56,700), so test/docs-readme-token-figures.test.ts went red. The banner is pinned to the measurement (解耦:banner 钉实测,ceiling 独立): README now says ~57k and the test docstring's worked row carries the band the 5% rule derives from it (54,150-59,850). The ceiling stays 59,000. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #2019
Clause-②: no
The account and contact deletion guards told the user to close the blocking records. Closing them did not help, because the app's own required lookups keep the parent. This PR keeps the rule and changes the guards' sentences. Each sentence now names what really frees the record. The account guard also counts what the engine counts.
Measured before the fix (the wrong answer at the public door)
These runs were on
dc58e047(@objectstack/*17.7.0), through@objectstack/verify's in-process handle. Every write was made as the admin throughhooks.run, the engine's write door, which is the door a RESTDELETEtakes.Account (F17). A customer account with one open deal:
DELETE_RESTRICTED, "Cannot delete customer account: 1 open opportunity still references it. Close or reassign it first."stage: closed_lost): 200.DELETE_RESTRICTED,dependentObject: crm_opportunity. This time the engine answers in its own words: "This Account is still referenced by 1 Opportunity record(s) through “Account”, which is required and cannot be cleared. Delete or reassign them first."RECORD_LOCKED. The closed-deal freeze refuses the move. The admin deletes the deal: 200. Then the account deletes.Contact (F18). A contact named on an activated contract:
DELETE_RESTRICTED,dependentObject: crm_contract: "This Contact is still referenced by 1 Contract record(s) through “Primary Contact”, which is required and cannot be cleared. …". An expired contract gives the same answer.On
dc58e047both⚠️ measured defectpins were green (test/hooks-runtime-sales.test.ts~638 and ~935), so the defect reproduced.What the repo says is meant: the history is kept, for both objects
A closed deal keeps its account.
crm_opportunity.crm_accounthas been a required lookup with no cascade since the app's baseline, so the engine refuses the delete while any deal names the account. The opportunity is privately shared and is not a detail of the account. Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read #549 made quotes and contracts master-detail under the account and left the opportunity as a lookup.opportunity_lifecyclefreezes a closed deal. Not even an admin can move it to another account (step 4 above).product_catalogrefuses to delete a product that past lines still use ("Set is_active=false to retire instead").opportunity_line_item.crm_productstays on the restricting default so that deleting a product does not destroy the priced history of the deals that sold it.An ended contract keeps its primary contact.
So both deletions stay blocked. No
deleteBehaviorchanges, and no record becomes deletable or undeletable that was not already. Only the guards' answers change.The fix
account_protection(src/sales/objects/account.hook.ts). The guard now counts every opportunity on a customer account, which is what the engine counts. It still writes one whole sentence per branch (account_hygiene 删除守卫在单数时主谓不一致:"1 open opportunity still reference it" #721):The envelope is unchanged:
DELETE_RESTRICTED/ 409.contact_integrity(src/sales/objects/contact.hook.ts). The counts are unchanged. The sentence's last part, "Close or reassign those records first.", becomes "Close or reassign the opportunities and quotes first, and give each contract another primary contact: a contract keeps its primary contact even after it ends." The envelope is unchanged.content/docs/sales/accounts(en, zh-Hans, zh-Hant). The deletion rule now says that closed deals keep the account and that retiring the account means setting it to Inactive.Changeset.
.changeset/2019-deletion-guards.md,'hotcrm': patch.Why this is the smallest fix. The required lookups already say what the business means, so the declarations stay as they are. Only the two places that misstated the rule change: the guards' sentences and the Accounts page.
A deviation from the dispatch's suggested route: the contact guard's contract count stays
activated. This guard also runs as a cascade child of an account delete, and it cannot tell the two apart (#693, pinned intest/cascade-guard-messages.test.ts). An account delete takes its draft and ended contracts with it (#549). If this guard counted ended contracts, it would refuse that account delete itself. That would turn the F8 sentinel red now, and it would still refuse after objectstack-ai/objectstack#22305 (the platform's cascade order) is repaired. On a direct contact delete, an ended contract is refused by the engine, whose message names the contract, and the guard's sentence already says that ending a contract does not release the contact.Tests that pin the business facts
a closed deal keeps its customer account: closing it changes nothing, deleting it frees the accountreplaces the F17⚠️pin. With an open deal, the guard refuses with its sentence. After the deal is closed, the guard refuses with the same sentence,DELETE_RESTRICTED/ 409. After the deal is deleted, the account deletes.an ended contract keeps its primary contact: ending it changes nothing, another primary contact frees the personreplaces the F18⚠️pin and runs for both terminated and expired contracts. While the contract is activated, the guard's sentence names the real remedy. Once it has ended, the engine refuses withDELETE_RESTRICTEDoncrm_contract. With another primary contact on the contract, the person deletes and the contract stays.test/cascade-guard-messages.test.ts.Ablation. Each run was done once and restored, under a
trap. No test file was added for it.dc58e047. The mutation was checked on disk withgrep -c: new sentence 0, old sentence 1, in each file. Result: the singular, plural and both replaced pins fail, for exampleReceived: "Cannot delete customer account: 1 open opportunity still references it. Close or reassign it first."andReceived: "… Close or reassign those records first.". The F8 pin stays green.Received: "This Account is still referenced by 1 Opportunity record(s) through “Account”, …". The singular sentence pin stays green, so this pin is what measures the count.git hash-objectequals the HEAD blob andgit diff HEADis empty.Gates
pnpm verifyon97949036: exit 0. It coversValidation passed, typecheck,objectstack lint --strict,i18n lint gate: 0,source hygiene clean,source token ratchet cleanandBuild complete. Tests:Test Files 172 passed (172),Tests 3567 passed | 1 skipped (3568).pnpm changeset status --since=origin/mainbumpshotcrm(patch).Left to CI, not run locally:
test:coverage(thresholds onsrc/*/objects/*.hook.ts), the docs-app build (apps/docs, a separate Next.js install) and link-check.Files outside the claim's declared surface
test/cascade-guard-messages.test.ts: its account_hygiene 删除守卫在单数时主谓不一致:"1 open opportunity still reference it" #721 end-to-end pins assert the old account sentence word for word. They have to follow the new sentence, orpnpm verifyfails.test/freeze-guard-reference-cleanup.test.ts: one comment said "account_protectioncounts OPEN opportunities". It would be false after this change, so it now says every deal counts. Code unchanged.Acceptance notes (seen, not filed)
pnpm verifyrun on the same sha failed one unrelated test:test/script-bodies.test.ts, "send_email writes the email and its timeline pointer", expectedqueued, receivedfailed. The file passed on its own (57/57) and in the second full run. This looks like a timing flake under full-suite load. Carrier: none.Generated by Claude Code