Skip to content

fix(revenue)!: remove crm_quote_line_item.tax_rate — a quote line's total is its subtotal - #2089

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-2069-quote-tax-rollup
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-2069-quote-tax-rollup

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #2069

What and why

This executes ruling B on the card (comment 6142078386, the maintainer's 「hotcrm 三个决裁都同意」): the quote's typed Tax is the only tax model.

Every reader of the field

git grep -n tax_rate over the whole tree at base 0f84c9c found the following. Views, pages, field groups, hooks, flows, datasets, reports, mappings, assets/import-templates/ and e2e/ have none.

Reader Change
src/revenue/objects/quote_line_item.object.ts The field and the multiplier are removed. Three comments are corrected: the group note, the related-list note and the formula note. "eleven fields" becomes "ten".
src/revenue/objects/opportunity_line_item.object.ts The twin's comment "its eleven fields" becomes "ten".
src/revenue/data/revenue.seed.ts The comment about leaving tax_rate at 0 is removed.
src/sales/translations/{en,es-ES,ja-JP,zh-CN}/objects.commerce.ts The 4 locale rows are removed.
content/docs/sales/quotes ×3 locales The line-items panel paragraph now says a line carries no tax.
content/docs/revenue/products ×3 locales The tax paragraph drops the line's Tax Rate %.
content/docs/whats-new ×3 locales See the deviations below.
test/line-item-conventions.test.ts, test/undeclared-key-probe.test.ts Re-pointed, see below.

CHANGELOG.md keeps its history. After this PR, git grep -n tax_rate -- src returns 0 hits, and the built artifact carries 0 "tax_rate" (base: 5).

Acceptance 2: one pin, red before and green after

The new test in test/hooks-runtime.test.ts (quote_total_rollup) uses QTE-0007's shape: a quote at 10% discount, an untaxed line of 2 × 5,000, and a line of 1 × 1,000 at 10% line discount. The rep then sets a 5% rate on the second line. It asserts that the line totals equal the header subtotal (10,900), that the total is 9,810, and that the rate write is refused as ['INVALID_FIELD', 400].

Red on the unfixed tree (HEAD 0f84c9c7, only the test file modified), under the shared lock:

FAIL  test/hooks-runtime.test.ts > quote_total_rollup > adds up on QTE-0007's shape: ...
AssertionError: expected { lines: 10945, subtotal: 10900, …(1) } to deeply equal { lines: 10900, subtotal: 10900, …(1) }
-   "lines": 10900,
+   "lines": 10945,
    "subtotal": 10900,
    "total": 9810,
os-verify-lock: VERDICT command-exit 1

It is green after the fix, inside the full pnpm test run below.

Re-pointed tests

  • line-item-conventions: "quote line total still applies tax on top" becomes "quote line total is its subtotal". Both formulas evaluate to 360 on 4 × 100 at 10%.
  • undeclared-key-probe: control D needed a key that is declared somewhere and undeclared on crm_opportunity_line_item. It moves from the quote line's tax_rate to crm_quote.tax: it is the same key, with the same value 10, and the stored value is read back. The R81 report suggested crm_quote (the lookup) instead. I did not use it because a lookup's value must be a real quote id, which breaks the same-key-same-value control and makes it depend on reference semantics. All 3 drivers pass.

What an existing install that filled a line rate sees (measured)

  1. I built base 0f84c9c7 and booted it on a scratch SQLite file.
  2. Over REST, I wrote QTE-0007's shape with line B at tax_rate: 5. The base reproduces the card: line B total 945, lines 10,945, header subtotal 10,900, total 9,810, tax null.
  3. I stopped it and booted this branch's build (9b4516d7) on the same file.
Reading after the upgrade Value
Boot log [schema-drift] crm_quote_line_item.tax_rate: column exists in the database but not in metadata (orphaned) — "os migrate apply --allow-destructive" to drop it.
Line B over REST total_price 900, and the response has no tax_rate key. Line A is 10,000.
Header Unchanged: subtotal 10,900, discount_amount 1,090, total_price 9,810.
PATCH line B {tax_rate: 5} 400 {"error":"Unknown field 'tax_rate' on object 'crm_quote_line_item'","code":"INVALID_FIELD"}
os migrate plan Destructive: ✗ crm_quote_line_item.tax_rate [drop_column]. It is the only destructive change.
os migrate unmapped-columns --object crm_quote_line_item --json columns: [{column: tax_rate, actual: float}], 22 rows; line B reads tax_rate: 5, every other row reads 0.
Console DOM probe on that quote The Related tab shows line Totals 900 and 10000 (sum 10,900), equal to the header Subtotal 10,900.00. Total Price is 9,810.00.

The changeset carries this: the line's total changes (945 → 900), the quote total does not, and the stored rates stay readable until os migrate apply --allow-destructive drops the column.

Browser check on a fresh DB

I ran pnpm dev on port 4001 against a fresh database and signed in as admin@objectos.ai. Then I wrote QTE-0007's shape over REST:

  • The rated line insert answered 400 INVALID_FIELD, so I wrote line B without the rate.
  • DOM probe (Chromium /opt/pw-browsers/chromium): {"rows":2,"sumLineTotal":10900,"sumLineSubtotal":10900,"headerSubtotal":10900,"headerTotal":9810,"agree":true}.
  • The line record page renders Sales Price and Total, and has no Tax Rate.

Gates

All gates ran on fe3f05ee, under the shared lock, with each exit captured before any pipe:

Command Verdict Exit
pnpm validate ✓ Validation passed, Data: 18 Objects 357 Fields 0
pnpm typecheck tsc --noEmit 0
pnpm lint 18 suggestions, the same pre-existing set and none from this change 0
pnpm lint:i18n-gate ✓ i18n lint gate: 0 i18n/missing-* issues 0
pnpm hygiene ✓ source hygiene clean 0
pnpm hygiene:tokens ✓ source token ratchet clean 0
pnpm build artifact built 0
pnpm test Test Files 179 passed (179), Tests 3691 passed, 1 skipped (3692); VERDICT command-exit 0 0

hygiene:tokens, src/revenue:

  • Authored total goes from ~18,986 to ~18,939: −47 against a ceiling of ~19,000, with headroom rising from 14 to 61.
  • Business semantics goes from ~15,846 to ~15,799.

Deviations and judgment calls

  • docs/STATUS.md was not in the claimed file surface. Its validator transcript goes from 358 to 357 Fields, because test/docs-declared-versions.test.ts pins it against the registered stack. The 3.1.0 precedent did the same, and the build transcript confirms 357.
  • whats-new. No feature PR writes a release section; the release PR writes the next release's whats-new section from the changesets, as 3.1.0's was written from 1486ec0c's. The ruling's "whats-new" item is the 3.1.0 entry that says tax "is actually computed" on "the quote line item's own rate". That entry keeps its dated text and gains a forward note in all three locales: a later release retires the line rate too.
  • The probe's control key is crm_quote.tax, not the crm_quote lookup R81 suggested. The reason is given above.

Acceptance notes (not changed here)


Generated by Claude Code

…ts subtotal

A quote line carried a Tax Rate % that reached only that line's Total, while
the quote rollup sums pre-tax subtotals and adds the quote's own typed Tax.
On a quote whose line carried a rate, the lines summed to more than the
header beside them (lines 10,945 against a 9,810 total on QTE-0007's shape).

The quote's typed Tax is the one tax model: it is seeded on every demo
quote, on the default form and grid, and is what the docs tell users to
fill. So the line rate is removed, and the line's total_price is the same
expression as its subtotal. The quote rollup never read the rate and is
unchanged: the header is still subtotal - discount + tax + shipping.

- crm_quote_line_item.tax_rate and its four locale rows are removed; the
  comments that described it (object, twin, seed) are corrected.
- The quotes and products pages say a line carries no tax, in all three
  locales; whats-new's 3.1.0 entry notes the later retirement.
- docs/STATUS.md's validator transcript goes from 358 to 357 fields.
- hooks-runtime pins QTE-0007's shape: the line totals add up to the
  header's subtotal and a rate on a line is refused (INVALID_FIELD / 400).
- line-item-conventions pins total_price == subtotal; the undeclared-key
  probe's declared control moves from the line's tax_rate to crm_quote.tax.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
States what an install that filled a line rate sees: that line's Total
changes (945 to 900 on a 1 x 1,000 line at 10% off and 5%), the quote's
Total Price does not, and the stored rates stay in an orphaned column that
os migrate plan lists as a destructive drop and os migrate unmapped-columns
reads by record id. Measured by upgrading a database written by the base
build.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 13, 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 13, 2026 10:36am 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 13, 2026
The quote line's tax rate is retired on this branch, so the row states what
the line total now is: the line after its own discount, with no tax of its
own.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

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

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.

A quote's total ignores line-item tax: lines total 10,945 while the quote shows 9,810 (the rollup sums pre-tax subtotals and never fills crm_quote.tax)

2 participants