Skip to content

Declare the auto-drafted contract's defaults as defaults (#1129) - #1523

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-1129-draft-contract-defaults
Sep 3, 2026
Merged

Declare the auto-drafted contract's defaults as defaults (#1129)#1523
os-sales merged 1 commit into
mainfrom
claude/issue-1129-draft-contract-defaults

Conversation

@os-sales

@os-sales os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1129

Implements option B of the maintainer's 2026-08-31 ruling: an auto-drafted contract is a starting draft an admin completes, not a faithful transcription of the quote. So none of the values change — what changes is that nothing marked them as placeholders, which is the same complaint #873 recorded one field over, and that one cost a real drift.

The drafted contract document is byte-identical. No behaviour change, pinned by a new test rather than argued.

What landed

1. The three defaults are declared as defaults. contract_term_months: 12, start_date: today and contract_type: 'subscription' are collected into a DRAFT_CONTRACT_DEFAULTS block carrying the provenance the ruling asked for: placeholder defaults, not business decisions, with the reason each one exists (the contract requires it; the quote cannot express it) and the unfreeze criterion for the alternative. end_date is documented as derived from two of them rather than as a fourth guess.

2. The other half is settled at the same site. A comment records that the quote's shipping_terms, billing_address, shipping_address and description are deliberately not copied — decided, not overlooked — with the structural facts behind that: two of them have no counterpart column on crm_contract at all, billing_address has one and is left for the admin completing the draft, and the contract's description is occupied by the draft's provenance sentence.

3. A pin test (test/quote-accepted-draft-defaults.test.ts, 12 assertions) covering the values themselves, the four fields that deliberately reach nothing, and the claim the provenance rests on.

Two things measured rather than recalled

contract_type really has no default of its own. The provenance comment's load-bearing claim is that this hook is the only thing that ever picks a contract type. Measured off the object: six options, no field-level defaultValue, and no option carrying default: true — the two spellings this repo actually uses (cf. status and billing_frequency on the same object, which do carry one). Both spellings are now pinned, so the comment goes red if that ever changes instead of quietly becoming false.

There is no second drafting path. A defaults block covering one of two drafting sites would be a half-fix, so this was checked rather than assumed: src/objects/quote.hook.ts is the only code path that creates a crm_contract row. No flow create_record node targets crm_contract; contract.hook.ts only updates and contact.hook.ts only counts. The seeded contracts in src/data/revenue.seed.ts are hand-authored demo rows — which is why they vary by type while every auto-drafted contract is a subscription.

Why the block is inside the handler

The ruling names a constant block; this file's own header names the constraint that decides where it can live. An L2 hook body ships body-only and has no module scope at runtime, so a hoisted block is not a style preference — it breaks the body-only path.

That is the one failure this refactor could actually introduce, so it is the one the test measures directly. Ablation, on the committed implementation: hoisting DRAFT_CONTRACT_DEFAULTS to module scope leaves 11 of 12 assertions green and turns exactly one red — the QuickJS leg:

Error: [hook-body-extract] hook 'quote_on_accepted': handler references identifier(s)
not in scope at runtime: DRAFT_CONTRACT_DEFAULTS. Module-scope helpers/imports aren't
shipped with a metadata-only body, so this handler will be BUNDLED instead

That asymmetry is the point: every ordinary closure-keeping test is blind to a hoisted block. The tree was restored from HEAD afterwards and the file's blob hash re-checked against HEAD:src/objects/quote.hook.ts (22c684b, identical).

Verification

pnpm verify green end to end at db06ad9 — validate, typecheck, lint, i18n gate, hygiene, token ratchet, build, and 157 test files / 3297 tests passed, 1 skipped. The token ratchet moved 83,079 to 83,156 in business semantics (ceiling ~85,000, headroom ~1,844); comments are stripped before that count, so the growth is the block itself.

Changeset

Empty frontmatter — the sanctioned "this PR releases nothing" declaration that .github/workflows/changeset-check.yml documents, on par with the skip-changeset label. Chosen over the label because this is not a CI or housekeeping chore: it is a source change in business logic, and the in-repo record of why those values are what they are belongs with it. Nothing reaches users, so a patch bump would put a line in the release notes about a document that did not change.

Scope

Only the ruling. #1253 (a drafted contract's back-link to the quote that produced it) is graded independently and is not folded in here. #714 (boolean false into a lookup) and #873 (the payment_terms copy) stay out of scope; neither is touched. Option A — the quote carrying a real term and type — is recorded in the code comment as recorded-not-undertaken, with its unfreeze criterion, and is not attempted.

🤖 Generated with Claude Code

https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t


Generated by Claude Code

`quote_on_accepted` supplies `contract_term_months`, `start_date` and
`contract_type` because `crm_contract` requires all three and a quote can
express none of them. The maintainer's 2026-08-31 ruling settled what they
are: an auto-drafted contract is a starting draft an admin completes, not a
faithful transcription of what was sold. So the values stay and their
provenance becomes visible — a `DRAFT_CONTRACT_DEFAULTS` block that says they
are placeholders rather than decisions, with `end_date` documented as derived
from two of them rather than as a fourth guess.

The block is handler-local, not module scope: an L2 hook body ships body-only
and has no module scope at runtime, so a hoisted block would `ReferenceError`
in production while passing every plain-function test. The new test's QuickJS
leg is what holds that down.

The ruling's other half is recorded at the same site: the quote's
`shipping_terms`, `billing_address`, `shipping_address` and `description` are
deliberately not copied.

No behaviour change — the contract document is byte-identical.

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

vercel Bot commented Sep 3, 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 Sep 3, 2026 7:45am UTC

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Sep 3, 2026
@os-sales
os-sales marked this pull request as ready for review September 3, 2026 07:50
@os-sales
os-sales added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit a8b2346 Sep 3, 2026
10 checks passed
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 metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

2 participants