fix: coerce money fields to integers before API calls - #17
Merged
Conversation
CHIP Collect requires money fields (price, discount, total_price_override, capture/refund amounts, totals and overrides) as integers in minor units. Apps computing sen via floating point (e.g. 0.29 * 100 = 28.999999999999996) sent fractional numbers, causing 400 'A valid integer is required.' from the API. Values within 1e-9 of an integer are now coerced; genuine fractional sen (e.g. 108.5) throws InvalidMoneyValueException instead of being silently truncated by PHP's implicit int cast (v2 builder previously sent 28 sen for 28.999...). - Add Chip\Support\Money::coerce() shared helper with conditional return type - Add Chip\Exception\InvalidMoneyValueException - Product: coerce price/discount/total_price_override in fromArray() and at jsonSerialize() time (covers direct property assignment, v1-style usage) - PurchaseDetails: coerce total/debt/overrides in fromArray() and jsonSerialize() - PurchaseBuilder: accept int|float|string for money params, coerce explicitly - PurchasesResource::capture()/refund(): accept and coerce amount - Add MoneyCoercionTest (15 tests): float noise, numeric strings, genuine fractions, NaN, serialization and builder paths
The Ollama Cloud API endpoint (api.ollama.com/api/generate) returns 410 Gone, so the workflow overwrites PR descriptions with its error output instead of a summary - destroying manually written PR bodies on every push. Remove the workflow and generator script entirely.
Release workflow extracts notes from the '## [X.Y.Z]' section matching the tag; prepare 2.1.0 so tagging works right after merge.
Manual process is preferred: the changelog is maintained by hand in the PR and releases are tagged manually instead of being driven by GitHub Actions.
- actions/checkout v6 -> v7.0.1 - actions/cache v5 -> v6.1.0 - shivammathur/setup-php v2 -> 2.37.2
LiyanaAmil
approved these changes
Aug 31, 2026
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.
Summary
CHIP Collect requires money fields (
price,discount,total_price_override,capture/refundamounts, totals and overrides) as integers in minor units (sen).Apps computing sen via floating point (e.g.
0.29 * 100→28.999999999999996) were sending fractional JSON numbers, and the API rejects them with:Verified live against the test-mode API:
108/108.0/"108.00"→ 201 OK28.999999999999996(float noise) → 400 (exactly the merchant-reported error)This is not a magnitude rule — it is binary-float rounding noise occurring for arbitrary values (any
ringgit × 100whose result is not exactly representable).Failure modes closed by this PR
PurchaseBuilderint $priceweak-mode coercion silently truncated sen (28.999…→28, wrong amount)108.5) explodes loudly instead of charging the wrong priceChanges
Chip\Support\Money::coerce()— shared helper: numeric within1e-9of an integer → that integer (covers float noise, int-valued floats, numeric strings like"108.00"); genuine fractional sen →Chip\Exception\InvalidMoneyValueException; null passes through.Product— coerceprice/discount/total_price_overrideinfromArray()and atjsonSerialize()time (public properties mean v1-style direct assignment still reaches the API clean).PurchaseDetails— coercetotal/debt/subtotal_override/total_tax_override/total_discount_override/total_overridein both directions.PurchaseBuilder— money params widened toint|float|string, coerced explicitly (no more silent weak-mode truncation).PurchasesResource::capture()/refund()— amount widened + coerced.InvalidMoneyValueExceptionextendsChipApiExceptionso existing catch blocks keep working.pr-summary.yml+scripts/generate_pr_summary.py— the Ollama endpoint now returns 410 Gone and the workflow was overwriting PR bodies with its error output (it corrupted this very PR's description on first push).Test Plan
composer test— 110 tests, 352 assertions OK (15 new intests/MoneyCoercionTest.php: float noise, numeric strings, genuine fractions, NaN, serialization + builder paths)composer phpstan— level 8, no errors (conditional return type onMoney::coerce)composer cs-check— clean201, fraction108.5rejected with precise message naming the field