fix(line-items): F/P expression tags, null guard, shared price-fill (#514 items 8, 3, 15) - #570
Merged
Merged
Conversation
Refs #514 (items 8, 3 and 15 — scoped to the two `*_line_item` objects). - `quote_line_item.total_price` was formula-on-formula (`record.subtotal * …`), depending on the platform hydrating another computed field first. Recomposed from the same stored fields `subtotal` reads; arithmetic unchanged. - `opportunity_line_item.unit_price_positive` compared `unit_price < 0` with no null guard. Strict CEL aborts on `null < 0`, so the rule was inert on a blank price — it now carries the `!= null &&` guard its quote-side twin has. - Formula fields were authored with `P` and `campaign_member`'s validation with a raw `{ dialect: 'cel', source }` object (the file never imported `P`). `F`/`P`/`cel` are the same tagged template, so this only ever misled readers. - The two price-fill hooks were near-verbatim copies differing in comments. Both now build from `_line-item-price-fill.ts`; the handler body closes over nothing but `ctx`, so it still lowers to a body-only sandbox callable and the two lowered bodies are byte-identical. Guarded by a new `test/line-item-conventions.test.ts` (its own file, not more surface on the high-churn `metadata-references.test.ts`). All six guards fail on main before these changes. `pnpm verify` passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomdmGNvi3GfNfBW53HJQb
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
July 31, 2026 08:51
3 tasks
yinlianghui
pushed a commit
that referenced
this pull request
Jul 31, 2026
Resolves conflicts in the two `*_line_item.hook.ts` files. main's #570 extracted the two near-verbatim price-fill handlers into a shared `_line-item-price-fill.ts`; that extraction wins, and the `where:` fix moves into the shared module — which had carried the `filter:` along with it. Widens the source scan in `test/hook-query-predicate.test.ts` from `src/objects/*.hook.ts` to every `.ts` in that directory. The narrow glob would have missed `_line-item-price-fill.ts` entirely: hook bodies get factored into shared modules, and a dropped predicate riding along in one of those is exactly as broken while matching no `*.hook.ts` pattern. The scan now asserts it can see that file, so the coverage cannot silently narrow again. The rollup fixes in both line-item hooks merged cleanly and are unchanged. pnpm verify: 25 files, 480 passed | 2 skipped. Coverage thresholds hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7h5XeGgNPBqNuV6LCRWpg
15 tasks
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.
Description
Partial follow-up to #514, scoped deliberately to the two
*_line_itemobjects (plus the one-linecampaign_membertag fix that belongs to the same item). Each of the four defects was re-verified as still present onmainat5b88ad0before being touched — the issue body predates several merged PRs.This PR does not close #514; the remaining tier-3/tier-4 items are untouched.
Type of Change
Related Issues
Refs #514 — items 8, 3 (line-item half) and 15 (price-fill half) only. Not a fix-all; the issue stays open.
Changes Made
crm_quote_line_item.total_pricewasrecord.subtotal * (1 + tax_rate/100), andsubtotalis itself a formula, so the total depended on the platform hydrating another computed field first (the hazard written up atlead.object.ts:61-64). It is now composed from the same stored fieldssubtotalreads, with the tax multiplier on top. The arithmetic is unchanged — 4 × 100 at 10% line discount and 8% tax is still 388.80 — what changes is that it no longer depends on evaluation order.crm_opportunity_line_item'sunit_price_positivecomparedrecord.unit_price < 0with no null guard. Strict CEL aborts onnull < 0rather than evaluating it false, so the rule was inert on a blank price — it never fired at all. It now carries the same!= null &&guard its quote-side twin has always had. The guard only narrows the predicate, so no previously-accepted record starts failing and no seed data changes; a blank price is still caught by the field's ownrequired.P(the predicate alias), andcampaign_member's validation used a raw{ dialect: 'cel', source }object because the file never importedP.F,Pandcelare all aliases of the same tagged template, so this was invisible at runtime and only ever misled readers. Formulas now useF, conditions useP, andcampaign_member.object.tsimportsP.opportunity_line_item.hook.tsandquote_line_item.hook.tscarried copies of the same price-fill handler differing only in comments — the shape that lets a fix land on one object and silently skip the other. Both now build from a newsrc/objects/_line-item-price-fill.ts. The sharing is authoring-time only: the handler body closes over nothing but its ownctx, so it still lowers to a body-only sandbox callable, verified against the build artifact (the two loweredbody.sourcestrings indist/objectstack.jsonare byte-identical and mention neither factory parameter). The rollup hooks next door look alike but compute genuinely different totals, so they are left alone.Testing
pnpm test— 22 files, 403 passed / 2 skipped)pnpm lint)pnpm build)pnpm verifyend to end, exit code 0New guards live in
test/line-item-conventions.test.ts— a new file on purpose, rather than more surface ontest/metadata-references.test.ts, which is the repo's highest-churn test module and would conflict with parallel #514 work.Written test-first: all six guards fail on
mainbefore the source changes, one per defect.Field.formulaexpression usesF*.object.tsconditionusesP(catches the raw{dialect:'cel'}literal too)*.object.tsunit_price_positiverules are null-guardedThe tag guards have to read source text because
F/P/celproduce byte-identical Expression envelopes — the distinction exists only in the authored source. The other guards assert against compiled metadata. Both tag guards run repo-wide oversrc/objects/*.object.tsand are clean after this change; the null-guard check is scoped to the two line-item objects becauseproduct.object.tshas the same defect and is tracked by the rest of #514 item 3.The pre-existing behavioural price-fill tests in
hooks-runtime.test.tsandhooks-runtime-sales.test.tsstill pass unchanged, which is the independent evidence that the de-duplication preserved behaviour on both objects.Screenshots
n/a — no UI surface changes.
Checklist
.changeset/line-item-expressions-and-price-fill.mdcampaign_enrollmentexpression warnings are pre-existing onmain)Additional Notes
The null guard on
unit_price_positiveis the one item here with a real behavioural footprint, and it is strictly narrowing: the rule fired on nothing before and now fires on a genuinely negative price. Worth a second look during review is the sandbox reasoning in_line-item-price-fill.ts— the factory is only safe as long as the handler body never reads a factory parameter, since that would resolve at authoring time and beundefinedinside QuickJS. That constraint is stated in the module's doc comment.Generated by Claude Code