fix(flows): round quote money to the fields' declared 2-decimal scale - #1700
Merged
Conversation
`quote_generation` wrote `discount_amount` and `total_price` as bare IEEE-754 products of a currency and a percentage. `discount / 100` is inexact for every percentage whose hundredth is not a dyadic rational, so the product carried a tail that `crm_quote`'s `scale: 2` money fields refuse: 180,000 at 30% is 125999.99999999999 and the insert was rejected with `Total Price must have at most 2 decimal places (got 11)`. Whether a quote could be created at all was an arithmetic accident of amount x discount — 20% of 180K worked, 30% of the same 180K did not — and the console never surfaced the 400, so the action looked like it did nothing at all. Round both products to the field scale inside the expression, using the CEL stdlib's `round()` as mirrored into flow value expressions by service-automation 17.3.0. That function is integer-only and single-argument, so N-decimal rounding is `round(x * 100) / 100`, the pattern the platform's own arity diagnostic names. No operator trick: `(x * 100 + 0.5 | 0) / 100` evaluates too, but `|0` is an int32 coercion that silently overflows above ~21.5M, which on a money field is worse than the defect it dodges. The regression pin asserts the VALUE, not the absence of an error: the harness's in-memory data engine does not enforce field scale, so a pin asserting "the run did not fail" would be green before and after. It covers both edited expressions — 30% leaves the tail on `total_price`, 70% moves it to `discount_amount`. Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-steve
marked this pull request as ready for review
September 6, 2026 11:47
12 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.
Fixes #1206
Generate Quotewas broken for most non-zero discounts on the flagship exemplar, and it failed silently. This rounds both money expressions to the fields' declared scale, using the numeric function table that@objectstack/service-automation17.3.0 shipped — the capability this card waited 13 days for.The artefact check came first, not the code
This card was unblocked on a read of the release record. Per R48's predicate the unlock had to be evaluated against the installed artefact, so that was the first action, before any edit. Both legs pass on the installed
@objectstack/service-automation17.3.0 (node_modules/@objectstack/service-automation/dist/index.js):KNOWN_EXPRESSION_FUNCTIONS=abs,round,floor,ceil,min,maxgrep -c "unknown function" dist/index.js= 1Grep proves presence, not behaviour, so the table was also exercised through the real
AutomationEngineon the real flow (180,000 opportunity,test/helpers/flow-harness.ts):Leg A reproduces the issue's measured value exactly, which is what confirms this is the live evaluator. D/E/F now abort the run with zero rows written; before 17.3.0 every one of them was rewritten to the literal
nulland the field was writtenundefined.The function table is not what the card assumed
round()is the CEL stdlib's, mirrored 1:1 — integer-only and single-argument. There is noround(x, 2). N-decimal rounding isround(x * 100) / 100, and the platform's own arity diagnostic names that pattern verbatim (leg D above), so this is the sanctioned spelling rather than an invention.Changes Made
src/flows/quote-generation.flow.ts— both money expressions round to the 2-decimal field scale:with a comment recording why, because the shape applies anywhere a flow multiplies a currency by a percentage.
test/flow-quote.test.ts— the regression pin, added to the existing runtime suite (no new gate, AGENTS.md:431).⛔ No operator trick.
(x * 100 + 0.5 | 0) / 100does evaluate, but|0is an int32 coercion that silently overflows above ~21.5M — on a money field that is worse than the defect being fixed.round()refuses loudly pastNumber.MAX_SAFE_INTEGERinstead.⛔ Not made
readonlyor formula fields.quote.object.tsrecords deliberately that these are not readonly because the line-item rollup writes them; that would be a schema redesign, not this card.subtotalis unchanged and re-verified as safe: a bare path pass-through ofcrm_opportunity.amount, itselfField.currency({ scale: 2 }).The pin asserts the VALUE, and it has to
The harness's in-memory data engine does not enforce field scale, so a pin asserting "the run did not fail" would be green before and after and would pin nothing. Ablation, run from the committed state — the fix reverted to the pre-fix bare products, the mutation proved on disk by blob hash (
bea2eb83vs2f42350e) before the suite ran:Restored by byte identity, not by an exit code: the blob hash returned to
2f42350eandgit diff HEADis empty.The two pre-existing cases (10% of 200,000, and 0%) stayed green through the ablation — they use discounts that are exact either way, which is precisely why they never caught this. The new pin runs 30% and 70% on a 180,000 opportunity so that both edited expressions are covered: at 30% the tail sits on
total_price, at 70% it moves todiscount_amount.Sibling scan (re-run, not quoted forward)
src/flows/re-scanned across all 25 flow files for arithmetic inside{...}tokens. Difference set vs the previous dev's scan: empty. The only currency × percentage sites are the two lines fixed here; every other hit is a cron schedule, aTODAY() + Ndate macro, or a wildcard path token. No sibling card to file.Verification
pnpm verifyfully green atac4f00e4— all eight stages ran (validate→typecheck→lint→lint:i18n-gate→hygiene→hygiene:tokens→build→test):The token ratchet stays clean with headroom: business semantics ~85,041 (ceiling ~100,000), interaction layer ~37,963 (ceiling ~40,000), authored total ~137,345 (ceiling ~140,000). The ratchet is comment-stripped, so the explanatory comment costs nothing.
Generated by Claude Code