Skip to content

fix(forms): a quote line no longer requires a Sales Price, and a mid-period Period Start is refused naming the start alone (#2060 items 7b, 7c) - #2196

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-2060-item-7bc-line-price-and-period-start
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-2060-item-7bc-line-price-and-period-start

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #2060 (items 7b, 7c)
Clause-②: no

The two residues of item 7 that comment 6281263541 folded onto the card, under claim 6286510541. #2060 remains open: this PR delivers items 7b and 7c only, and the card's other open items are untouched.

7b. A quote line no longer requires a Sales Price the server fills

crm_quote_line_item.unit_price drops required and keeps storage.notNull. quote_line_item_price_fill (the shared _line-item-price-fill.ts) stays the producer. This is PR #2185's A-21 change on the twin line object, so the two line objects declare the price the same way, as the shared fill's header asks.

Measured as the rep na.rep@objectos.ai on the rep's own draft quote, with the product Implementation Services (list price 75,000). Each column is a dev server booted from a fresh database with demo:staff. Console readings are headless Chromium at 1440x900.

Probe main e6d5d3e this branch 8e2d307
Console, Related, Quote Line Items, New: a product picked, no price Sales Price marked required. Toast "Please check the highlighted fields: Sales Price". 0 requests sent Sales Price not marked. POST without unit_price answers 201, toast "Quote Line Item created successfully", and the list shows Sales Price 75,000.00
REST POST /api/v1/data/crm_quote_line_item without a price 201, unit_price 75000 = list_price 201 (the console POST above)
REST PATCH with unit_price: null 400 VALIDATION_FAILED "Sales Price is required and cannot be cleared" 400 VALIDATION_FAILED "unit_price is required", stored price unchanged. The NOT NULL refusal's wording is objectstack#24715's, as on deal lines
Import preview: engine validate, then REST import with dryRun: true engine: "Sales Price is required" engine: no errors. REST: 200, ok 1

7c. A mid-period Period Start is refused naming Period Start alone

Cause (mechanism assumption 2, confirmed): forecast_derive_period derives an end from an off-boundary start, and period_end_matches_calendar_period then judges it. Its arithmetic, start + one period − 1 day, matches the calendar period only for a start the two start rules admit.

Console, before and after. The sales manager opens New Forecast, keeps Period Month, enters Period Start 2026-10-15 and leaves Period End blank. The form sends {period: month, source: scheduled, period_start: 2026-10-15}.

  • main: the toast reads "Period Start must be the first day of the period — e.g. 2026-08-01 for Aug 2026.; Period End must be the last day of the period — …". The answer is 400 with two rule violations.
  • This branch: the toast reads "Period Start must be the first day of the period — e.g. 2026-08-01 for Aug 2026." and nothing else. The answer is 400 with one rule violation.

The two remedies the card names, both measured. Each is a scratch edit made through ablation-replace.mjs, booted on the real engine as the manager (hooks.run), and restored to a blob identical to HEAD. In the table, START, QTR and END stand for the start, quarter-boundary and window refusals.

Write main A: the hook stops deriving B: the end rule stands down (shipped)
month 2026-10-15, no end (the card's row) START + END START START
quarter 2026-10-15, no end START + QTR + END START + QTR START + QTR
month 2026-10-15, typed 2026-10-31 (October's real last day) START + END START + END START
quarter 2026-08-01, typed 2026-09-30 (Q3's real end) QTR + END QTR + END QTR
an edit moving only the start, 2026-09-01 to 2026-09-15 START + END START + END START
month 2026-10-15, typed 2026-11-14 START START START
month 2026-11-01, no end 201, end 2026-11-30 same same
month 2026-11-01, typed 2026-12-15 END END END
{period: quarter} only (the nightly sweep's shape) 201, Q3 2026 same same

On main the end rule refused a correct calendar end (2026-10-31) and admitted a non-calendar one (2026-11-14). Its verdict on an off-boundary start meant nothing.

B is shipped. Each branch of the end rule now also requires the start that its period opens on, written in the start rules' own patterns. So the rule stands down exactly when a start rule refuses the row. The hook is unchanged. The choice, on the four axes:

  • Real business need (measured). Three writers set a forecast start. The console form (a sales manager) and the revenue_forecasting skill are two of them. The third, forecast_snapshot, sends period only, and neither remedy touches it (last row). Both remedies fix the card's row. A leaves three shapes a person reaches in the form, and the console names Period End on each of them:

    • a typed end that is the month's real last day (Period End sits beside Period Start on the form);
    • an edit that moves only the start (the record form posts the stored end it shows);
    • Q3's real end under a month start.

    B names the start alone on every one of them.

  • Long-term soundness. The object already documents the three rules as "one set enforcing one invariant", with the end rule's claim true "ONLY because the two rules above pin the start". B writes that premise into the predicate. A would teach the hook what a boundary is, a second place that knows it. The object's own note argues against that ("These rules are the ONE enforcement point — deliberately not a throw in forecast.hook.ts as well"), and so does the end-boundary suite's scope pin.

  • Making AI errors harder. B is not tolerance. The set of refused rows is unchanged (in the table, every row refused on main is refused under B, and every admitted row is admitted). Only the second message goes, and the refusal names the one field to fix. Under A, an agent reading the hook would find boundary logic that reads like enforcement.

  • Startup focus. Both are one-expression changes. Neither adds a field, a rule or a gate. B touches one predicate and no hook.

REST on this branch, as the manager:

  • month 2026-10-15: 400, the start message only;
  • quarter 2026-10-15: 400, the start and quarter messages;
  • month 2026-10-01 with a typed end 2026-10-15: 400, the window message;
  • month 2026-11-01: 201, end 2026-11-30, label "Nov 2026".

Docs

content/docs/sales/forecasting (en, zh-Hans, zh-Hant) gains one sentence on the period_end bullet: while period_start is off its boundary, the refusal names period_start alone. content/docs/sales/quotes already says a line's unit price defaults to the product's list price. The form now honours that, so the page is unchanged. content/docs/ was grepped uncapped in all three locales for the price and the period rules.

Tests

  • test/form-required-server-defaults.test.ts:
    • crm_quote_line_item.unit_price joins the NOT NULL server-filled declaration pin.
    • The Sales Price block runs on both line objects: a line with no price lands at list, a negotiated price is kept, the dry-run preview reports no missing price, and a clear on update is refused by the column.
  • test/forecast-period-boundary.test.ts: every mid-period start the suite refuses now asserts that no Period End message comes with it.
  • test/forecast-period-end-boundary.test.ts, new block "the window rule stands down while a start rule fires":
    • four inserts and one edit, each refused by the start rules alone (the messages are read off the declared rules);
    • the control: once the start is on its boundary, a short end is the window refusal again.
  • The existing pins still hold: a first-of-period start with a blank end derives its end, and an explicit wrong end on a boundary start is refused by the end rule.

Full suite on 8e2d307, 3 lock calls of vitest run --shard=k/3 --maxWorkers=2, each VERDICT command-exit 0. Together that is 202 files, equal to the count of git ls-files test files:

Shard Files Tests
1/3 68 passed (68) 1113 passed (1113)
2/3 67 passed (67) 1249 passed, 1 skipped (1250)
3/3 67 passed (67) 1641 passed (1641)

Ablations on 131cf84, one per changed behaviour, each through the lock with objectstack/scripts/ablation-replace.mjs. The anchor hit once and the blob moved; the restore read blob == HEAD with an empty git diff HEAD.

  • 7b puts required: true back on the quote line's unit_price. test/form-required-server-defaults.test.ts reads 3 failed | 21 passed: the declaration pin, the preview and the clear refusal, all on the quote leg. The first attempt was a no-op: its anchor was a prefix of its replacement, so the tool refused it (anchor count 1 to 1) and restored before any test ran. The second attempt is the one reported here.
  • 7c restores the old end-rule predicate. The two forecast suites read 9 failed | 68 passed: four start-refusal rows and five stand-down rows, with the control green.

Gates on 8e2d307

The verify chain ran in one lock hold, VERDICT command-exit 0. Each exit code was captured before any pipe.

Gate Exit Reading
pnpm validate 0 "Validation passed"
pnpm typecheck 0 no output
pnpm lint 0 16 suggestions, 0 errors
pnpm lint:i18n-gate 0 0 i18n/missing-* issues
pnpm hygiene 0 "source hygiene clean"
pnpm hygiene:tokens 0 src/sales business semantics ~57,434 (ceiling ~88,500); src/revenue ~17,991 (ceiling ~24,000)
pnpm build 0 "Build complete"
pnpm test 0 the three shards above

The control-byte self-scan over the 9 changed files exits 1 (none found).

Acceptance notes

  • Two fields labelled "Period". On the New Forecast form in the en locale, period (the picklist) and period_label (the free-text label) both read "Period": src/sales/translations/en/objects.pipeline.ts gives period_label the label 'Period', while the field declares "Period Label". This is outside this PR's surface and is reported to the seat.
  • demo:staff exits 1 on a fresh database. It reports "crm_task routed no rows at all" and the same for crm_event. This happened identically on main and on this branch, and is unrelated to this change. No one carries it.

Generated by Claude Code

…fills (#2060 item 7b)

crm_quote_line_item.unit_price was `required`, so the New Quote Line Item
form refused "Please check the highlighted fields: Sales Price" and sent
nothing, while the shared price fill (quote_line_item_price_fill) stores
the product's list price on every insert that leaves it blank (measured on
18.0.0-next.5: REST insert without a price, 201 with unit_price 75000 =
list_price). This is item 7's A-21 change on the twin line object:
`required` is dropped, `storage.notNull` keeps the column whole, and the
hook stays the producer, so both line objects declare the price alike.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
…rt alone (#2060 item 7c)

On a start off its calendar boundary, period_end_matches_calendar_period
judged a window that is no calendar period. Measured on 18.0.0-next.5: New
Forecast, Month, Period Start 2026-10-15 and no Period End answered 400
with "Period Start must be the first day …; Period End must be the last
day …", because the hook derives an end and the end rule judged it. The
same rule refused a typed 2026-10-31 and an edit that moved only the start,
and passed a typed 2026-11-14.

Each branch of the end rule now also requires the start its period opens
on, in the start rules' own patterns, so the rule stands down exactly when
a start rule refuses the row. Of the two remedies the card names, this one
was measured to cover every shape above; stopping the hook's derivation
cleared only the blank-end insert. The hook is unchanged.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
…fusal (#2060 items 7b, 7c)

7b: crm_quote_line_item.unit_price joins the NOT NULL server-filled
declaration pin, and the Sales Price block runs on both line objects: a
line with no price lands at list, a negotiated price is kept, the dry-run
preview no longer reports the price missing, and a clear on update is
refused by the column.

7c: every mid-period start the boundary suite refuses now names no Period
End, and a new block pins the window rule standing down while a start rule
fires (the blank-end insert, a typed calendar end, an edit that moves only
the start) with the control that it judges the end again once the start is
on its boundary.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
… names (#2060 items 7b, 7c)

forecasting (en, zh-Hans, zh-Hant) gains one sentence on the period_end
bullet: while period_start is itself off its boundary, the refusal names
period_start alone, and the end is judged once the start opens a real
period. The quotes page already says a line's unit price defaults to the
product's list price, which the form now honours, so it is unchanged. The
changeset describes both items for the release notes.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 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 20, 2026 9:28am 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 labels Oct 20, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT (#2060 items 7b, 7c) · PR #2196 at 8e2d307b4 · repo:hotcrm seat 2 · os-support-ai · session session_01KqkTQNLcxVc588eZpJmp1r · 2026-10-20T09:32Z · report 6287890416 on hotcrm#2060

  1. Shape:
    • Draft, base main. It opens with Part of #2060 (items 7b, 7c) and Clause-②: no, and the body carries no closing keyword. Assignee os-support-ai.
    • 9 files, +127 / −16, all inside the claim's surface; forecast.hook.ts is unchanged. check-governed-merges --pr: no governed path.
    • 0 model-identifier lines in the commits. Its base e6d5d3e9 is the current main.
  2. The diff, read:
    • 7b: crm_quote_line_item.unit_price drops required and keeps storage.notNull, PR fix(forms): stop requiring values the server fills on a line item, a quote and a forecast (#2060 item 7) #2185's A-21 change on the twin line object. The price fill stays the producer, and the comment points at the twin. The two line objects are aligned again, as _line-item-price-fill.ts asks.
    • 7c, remedy B: period_end_matches_calendar_period also requires the period's start to sit on its boundary (…-01 for a month, …-(01|04|07|10)-01 for a quarter), so it stands down while a start rule fires.
      • The != null guards and the daysBetween form are kept. The string() read of the start is noted against a JS Date, which no writer here sends.
      • Measured over 9 write shapes, the refused and admitted row sets equal main's, and only the message changes.
  3. The route, the seat's reading: B over A is measured, not chosen by taste. A cleared only the blank-end insert and still named Period End on three other shapes. B names Period Start alone on all of them and leaves the hook as the end's only producer.
  4. Changeset and docs, read sentence by sentence:
    • Both bullets match the diff.
    • "clearing a quote line's Sales Price is still refused; as on opportunity lines, that message names the field unit_price" is true, and it is objectstack#24715's.
    • The dry-run sentence follows from dropping required.
    • The forecasting page gains one sentence, in all three locales.
  5. Gates:
    • The dev's verify on 8e2d307b: every build-half step exit 0, and the three shards (68, 67 and 67 files; 1113, 1249 + 1 skipped and 1641 passed) each command-exit 0.
    • Browser before and after at 1440×900, the after-leg on a rebuilt artifact (the stale first reading was discarded).
    • Ablations: 7b required restored → 3 red; 7c old predicate restored → 9 red. Each restore is proven blob-equal.
    • CI on 8e2d307b4 at this review: 9 checks success; Build and Test (22.x) is running. Landing waits for every check.
  6. Out of scope, dispositions:

Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing main in first if it has moved (PR #2195 may land first). After the merge, #2060 goes back to pm:queue for its remaining items.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Development

Successfully merging this pull request may close these issues.

2 participants