Repository navigation
fix(quotes): an accepted quote no longer makes its contact, or the account above it, undeletable - #2183
Merged
objectstack-fleet[bot] merged 4 commits intoOct 19, 2026
Conversation
…/issue-2161-accepted-quote-frees-contact Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
…count above it, undeletable (#2161) crm_quote.crm_contact's requiredWhen becomes a gate on the write INTO presented or accepted (and while presented), reading `previous`, so the engine's referential clear on an already-accepted quote lands (#720) while no contract is drafted without a recipient (#1017). contact_integrity stops counting an accepted quote, and its refusal names only what holds the contact, with real plurals (#2060 item 12, A-17), and only remedies that free it. The stale "no cascade marker" note is corrected (comment only). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
… why the explicit guard stays (#2161) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
…es-contact Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Author
席位核对ACCEPT (#2161, the maintainer's ruling A
|
os-support-ai
pushed a commit
that referenced
this pull request
Oct 20, 2026
Brings in #2182 and #2183. Neither touches the seed, the escalation flows or the tests this branch edits. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
os-support-ai
pushed a commit
that referenced
this pull request
Oct 20, 2026
quote.object.ts merged without a conflict: #2183 narrowed crm_contact's requiredWhen, this branch declares expiration_date's default; both kept. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 20, 2026
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 #2161
Clause-②: no
What this changes
This carries out the maintainer's ruling A on the card (comment
6267342596). The accepted half of #1017's contact rule becomes a gate on the write that accepts the quote, andcontact_integritystops counting an accepted quote.crm_quote.crm_contactrequiredWhen(src/revenue/objects/quote.object.ts)has(record.status) && (record.status == "presented" || record.status == "accepted")has(record.status) && (record.status == "presented" || (record.status == "accepted" && (previous == null || !has(previous.status) || previous.status != "accepted"))). This is the ruled predicate, unchanged. next.5 has no declared transition-gate spelling: the field andstate_machineschemas offer none.contact_integrityquote count (src/sales/objects/contact.hook.ts)$nin: ['rejected', 'expired']$nin: ['accepted', 'rejected', 'expired']. Only open quotes hold a contact, as only open deals do.description, en and zh-CN packs)Comment block on
crm_contact: it now states the gate and why #720 and #1017 both hold. It also corrects the stale claim that an unevaluable predicate is skipped: since 17.0.0-rc.2 the write is rejected.Riders (ruling item 5):
contact.hook.ts(comment only): since 18.0.0-next.2 the platform declaresctx.referentialCascadeDelete, and this guard does not read it;content/docs/guides/import-and-export.mdx"Data deletion requests" (with both locale twins), checked against the new behaviour.content/docs/sales/quotes.mdxstates the quote-contact rule and the accepted-quote freeze, so it is updated too (the freeze sentence in both twins as well).Measurements (REST as a signed-up
sales_manager, through the shared lock)57d98535(main, next.5)DELETE /data/crm_contact/ID, the only quote acceptedDELETE_RESTRICTED"… 1 active quote(s) …"accepted,crm_contactnull, every other column equal to before exceptupdated_at/updated_byDELETE /data/crm_account/ID, the accepted quote on another accountcrm_contactnullDELETE /data/crm_account/ID, the accepted quote under that accountDELETE_RESTRICTED, nothing movesPM assumption 1 held on next.5, and so did assumption 2: a presented quote still blocks, and blanking its contact is still refused (the
quote-contact-required-whenenforcement pins are green).One new reading on the live path. With the contract still naming the contact, the delete is now refused by the engine itself: "This Contact is still referenced by 1 Contract record(s) through “Primary Contact”, which is required and cannot be cleared." That is the contract rule #2019 documents.
Stop condition: the console form and
previous(ruling item 6)Headless Chromium at
/opt/pw-browsers/chromium, 1440×900,pnpm dev, a fresh database, signed in assales.manager@objectos.ai(frompnpm demo:staff). It was measured on the probe diff, then again on the final head19cf6f0b.Contact, no required marker, lookup trigger withoutaria-requiredPATCH /api/v1/data/crm_quote/ID {"crm_contact":null}. It answers 200 ("Quote updated successfully"), and the stored row isacceptedwith contact nullContact*with the required marker, triggeraria-required="true"Contact, not requiredNo
[object-ui] A conditional predicate failed to evaluatewarning was logged. The form binds the persisted row aspreviousin edit mode. The stop condition did not trigger: the form shows the contact as optional on an accepted quote, and it does not refuse the save the server allows.That save also shows the trade the ruling accepted. A user can now clear an accepted quote's contact by hand, because
quote_workflow's #720 yield admits any caller's link clear.Tests and gates (final head
19cf6f0b)pnpm verify, split as the dispatch asks, each throughos-verify-lock.sh:validate,typecheck,lint,lint:i18n-gate,hygiene,hygiene:tokensandbuild, each exit 0,VERDICT command-exit 0;vitest run --shard=1/3:Test Files 66 passed (66),Tests 1094 passed (1094),VERDICT batch-last-exit 0(the shard command was the last part);vitest run --shard=2/3:66 passed,Tests 1244 passed | 1 skipped (1245),VERDICT command-exit 0;vitest run --shard=3/3:66 passed,Tests 1590 passed (1590),VERDICT command-exit 0.test/accepted-quote-frees-contact.test.ts(8 tests, REST door, sales manager), each pin naming its shape:code+status, andfields[]=crm_contact:required).test/cascade-guard-messages.test.ts(2 assertions) andtest/hooks-runtime-sales.test.ts(2 assertions). Intest/quote-contact-required-when.test.tsonly the source-text assertion changed; every enforcement pin is unchanged and green.scripts/ablation-replace.mjs(anchor hit, blob change and restore proven on disk), run against the new pin file:2 failed | 6 passed. Red: direct delete and account delete (quote elsewhere).accepted:3 failed | 5 passed. Red: all three delete shapes.previous == null ||dropped: stayed green,8 passed. On next.5,has()on a nullpreviousalso answers false, so the two guards overlap on an insert. The explicit term is kept as ruled; the comment records the reading.opportunity(ies)):1 failed | 7 passed. Red: the sentence test.git diff HEADis 0 bytes.Deviations
test/hooks-runtime-sales.test.tspins the old refusal sentence verbatim (two assertions), and the ruled sentence rewrite turns them red.src/sales/translations/en/objects.commerce.tsandsrc/sales/translations/zh-CN/objects.commerce.tscarry the Contact help text, which read as an invariant once the rule became a gate. Thedescriptionline beside the predicate changed for the same reason.Acceptance notes
test/quote-contact-required-when.test.tsheader still says a predicate that cannot evaluate is "SKIPPED with a warning". That has been stale since 17.0.0-rc.2. It was left alone because the dispatch limited that file to the source-text assertion. Carrier: none.content/docs/sales/quotes.zh-Hans.mdxand.zh-Hant.mdxhave never carried the English page's "A quote cannot be Presented or Accepted without a contact" bullet or its admin tip on the Contact requirement. That is pre-existing drift, not added here. Carrier: none.test/quote-line-freeze.test.tsrecords a dated next.3 reading: an account delete with an accepted quote was refused bycontact_integrity, so its account case uses an expired quote. The reading stays true as dated; an accepted case is now reachable. Carrier: none.crm_contract.allowDelete: false, Sharing coverage gap: territory/team-shared accounts show quote, task and contract related lists the rep cannot read #549). This is existing design and outside this card.Generated by Claude Code