Skip to content

Refuse a price that is a fraction of a minor unit - #2690

Open
kha333n wants to merge 2 commits into
lunarphp:1.xfrom
kha333n:fix/reject-decimal-prices
Open

Refuse a price that is a fraction of a minor unit#2690
kha333n wants to merge 2 commits into
lunarphp:1.xfrom
kha333n:fix/reject-decimal-prices

Conversation

@kha333n

@kha333n kha333n commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #2689.

1.x only — 2.x has replaced this cast, and I have not checked its behaviour.

A price written as 12.99 is stored as 13, and the column counts minor units,
so the product sells for AED 0.13.

The fix

set() returned the value unchanged, so a decimal reached a bigint column and
was rounded. It now refuses a value that is not a whole number of minor units,
and says what was probably meant:

Prices are stored as a whole number of minor units, so [price] cannot be 12.99.
Did you mean 1299?

Refusing rather than converting is deliberate: the cast cannot tell whether a
bare 12 is twelve minor units or twelve of the major unit — both are in use —
and guessing would multiply every correctly-written price by the currency factor.
The documented contract is already an integer count of minor units, so this makes
the code agree with the docs rather than changing the contract.

get() is untouched.

The existing tests

PriceTest passes a decimal in four places. Correcting those inputs to the
minor-unit equivalent leaves every assertion in the file unchanged->value,
->decimal and ->formatted() all still hold:

line was now
74 12.99 1299
91 12.995 (3dp currency) 12995
153 12.99 1299
154 13.99 (compare_price) 1399

They only appeared to work because the test asserts on the in-memory model and
never reloads it — get() strips non-digits, so "12.99" reads back as 1299
in the request that wrote it, and 13 in every request after.

Tests

tests/core/Unit/Base/Casts/PriceTest.php:

  • can not set a price to a fraction of a minor unit — over price and
    compare_price.
  • can say what the price was probably meant to be — the message carries
    1299, so the fix is actionable rather than just a refusal.
  • can set a price to a whole number of minor units — an integer, an integral
    float and a numeric string all still store 1299. Passes before and after, and
    is the guard that this rejects fractions rather than non-integers.
  • can leave a null compare price alone.

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

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

A price written as a decimal is silently stored as that many minor units

1 participant