Skip to content

fix(quotes): an accepted quote no longer makes its contact, or the account above it, undeletable - #2183

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-2161-accepted-quote-frees-contact
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-2161-accepted-quote-frees-contact

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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, and contact_integrity stops counting an accepted quote.

Where Before After
crm_quote.crm_contact requiredWhen (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 and state_machine schemas offer none.
contact_integrity quote 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.
The refusal sentence "… 0 open opportunity(ies), 1 active quote(s), 0 active contract(s) … Close or reassign the opportunities and quotes first, …" It names only what holds the contact, with real plurals ("1 open opportunity and 2 open quotes"), and only steps that free the contact. For a quote that means another contact, or rejecting it. Accepting it does not free the contact: the drafted contract names the same person and keeps its primary contact for life.
The Contact field's help text (description, en and zh-CN packs) "Required once the quote is Presented or Accepted …" (an invariant reading) "Required on the save that moves the quote to Presented or Accepted …". es-ES ("pasa a") and ja-JP ("になった時点で") already describe the gate and are unchanged.

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):

Measurements (REST as a signed-up sales_manager, through the shared lock)

Shape 57d98535 (main, next.5) this branch
Direct DELETE /data/crm_contact/ID, the only quote accepted 409 DELETE_RESTRICTED "… 1 active quote(s) …" 200. The quote is kept: status accepted, crm_contact null, every other column equal to before except updated_at / updated_by
DELETE /data/crm_account/ID, the accepted quote on another account 409, same sentence 200. The quote is kept on the other account with crm_contact null
DELETE /data/crm_account/ID, the accepted quote under that account 409, same sentence 200. The account, contact and quote are gone, because the quote is the account's master-detail child. "The quote survives" cannot apply to this shape (ruling item 4)
Live path: draft, in_review, presented, accepted through PATCH, then the drafted contract given another primary contact 409 "… 1 active quote(s) …" 200. The quote stays accepted with contact null
CONTROL: a presented quote, direct and account delete 409 409 DELETE_RESTRICTED, nothing moves
Rejected or expired quote 200 200 (unchanged)

PM 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-when enforcement 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 as sales.manager@objectos.ai (from pnpm demo:staff). It was measured on the probe diff, then again on the final head 19cf6f0b.

Quote Edit Quote dialog, Contact field Result
Accepted (QTE-0002) label Contact, no required marker, lookup trigger without aria-required Removing the contact and clicking Update sends PATCH /api/v1/data/crm_quote/ID {"crm_contact":null}. It answers 200 ("Quote updated successfully"), and the stored row is accepted with contact null
Presented (control) Contact* with the required marker, trigger aria-required="true" unchanged
Draft (control) Contact, not required unchanged

No [object-ui] A conditional predicate failed to evaluate warning was logged. The form binds the persisted row as previous in 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 through os-verify-lock.sh:
    • build half: validate, typecheck, lint, lint:i18n-gate, hygiene, hygiene:tokens and build, 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.
    • 198 test files ran, which is the number on disk.
  • New test/accepted-quote-frees-contact.test.ts (8 tests, REST door, sales manager), each pin naming its shape:
    • direct delete;
    • account delete with the quote on another account;
    • account delete with the quote under the deleted account (deleted with it);
    • presented-quote control on both doors;
    • the sentence's plurals and remedies;
    • the gate: accepting a presented quote while clearing its contact is refused; a quote born accepted with no contact is refused.
    • Refusals are asserted by envelope (code + status, and fields[] = crm_contact:required).
  • Pins rewritten by the sentence change: test/cascade-guard-messages.test.ts (2 assertions) and test/hooks-runtime-sales.test.ts (2 assertions). In test/quote-contact-required-when.test.ts only the source-text assertion changed; every enforcement pin is unchanged and green.
  • Ablations, each through scripts/ablation-replace.mjs (anchor hit, blob change and restore proven on disk), run against the new pin file:
    • A1, the old predicate back: 2 failed | 6 passed. Red: direct delete and account delete (quote elsewhere).
    • A2, the count keeps accepted: 3 failed | 5 passed. Red: all three delete shapes.
    • A3, previous == null || dropped: stayed green, 8 passed. On next.5, has() on a null previous also answers false, so the two guards overlap on an insert. The explicit term is kept as ruled; the comment records the reading.
    • A4, the plural shape back (opportunity(ies)): 1 failed | 7 passed. Red: the sentence test.
    • After all four legs, both files' blobs equal HEAD and git diff HEAD is 0 bytes.
  • CI runs the rest.

Deviations

  • File surface. The claim's surface did not name three files, and the change needed them:
    • test/hooks-runtime-sales.test.ts pins the old refusal sentence verbatim (two assertions), and the ruled sentence rewrite turns them red.
    • src/sales/translations/en/objects.commerce.ts and src/sales/translations/zh-CN/objects.commerce.ts carry the Contact help text, which read as an invariant once the rule became a gate. The description line beside the predicate changed for the same reason.
    • Each edit is one line or two assertions.

Acceptance notes

  • The test/quote-contact-required-when.test.ts header 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.mdx and .zh-Hant.mdx have 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.ts records a dated next.3 reading: an account delete with an accepted quote was refused by contact_integrity, so its account case uses an expired quote. The reading stays true as dated; an accepted case is now reachable. Carrier: none.
  • On the live path the account delete as the sales manager answers 403, because the drafted contract is a master-detail child the manager may not delete (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.
  • In the browser, the accepted quote's Edit Quote dialog showed Quote Owner empty ("Select…") for the sales manager. Not investigated; noted only.

Generated by Claude Code

…/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
@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 19, 2026 2:49pm UTC

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Oct 19, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT (#2161, the maintainer's ruling A 6267342596, p2) · PR #2183 at 19cf6f0baa · repo:hotcrm seat · os-project-manager · session session_01BDgipbNowaGa139em8Fhoq · 2026-10-19T14:54Z · report on hotcrm#2161 (6270122953); surface amended at review (6270189500)

  1. Shape:
  2. Premise, re-measured on next.5 (0336ad00, REST as a sales manager): an accepted quote gives a 409 on the direct delete and on both account-delete shapes, and on the live path. Rejected and expired answer 200.
  3. The diff, read, against the ruling's items:
  4. Accepted deviation: three files outside the named surface. Recorded on the card at review (6270189500, the adjacency check passes on all four conditions):
    • test/hooks-runtime-sales.test.ts, the old sentence pin;
    • the en and zh-CN Contact help text, which stated an invariant this change makes false;
    • the description beside the predicate.
  5. Gates:
    • pnpm verify on 19cf6f0b through the lock: the build half exits 0, and the shards (66 + 66 + 66 files) exit 0 / 0 / 0.
    • Ablations on the committed head, each proven restored:
      • the old predicate turns 2 pins red;
      • the old count turns 3 red;
      • the old plural shape turns 1 red;
      • dropping previous == null stays green, because has() on a null previous answers false on next.5. The term is kept as ruled, and the comment records why.
    • CI on 19cf6f0b: 9 of 10 success. Build and Test (22.x) was running at review time.
  6. Changeset, read: a patch written for an admin handling a data-deletion request. Each claim matches the measurement:
    • both deletes go through;
    • the accepted quote stays with its Contact cleared;
    • an accepted quote under the deleted account goes with the account;
    • the refusal names only open work, with real plurals;
    • the Contact rule still holds on the save into Presented or Accepted.
  7. Out of scope: none reported. pnpm demo:staff straight after a fresh boot races the background seed: pre-existing, already recorded on PR chore(deps): upgrade the ObjectStack platform to 18.0.0-next.5 #2181 → Acceptance notes.
  8. Landing: ready plus auto-merge once every check on this head reads success and the head still contains main's tip.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 15:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit b6cadbd Oct 19, 2026
11 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

2 participants