Fix Store API response for very large price amounts (#49361)
* Fixed overflow when formatting price for Store API responses * Added explanation comment. * Added changelog. * Linting. * Ensure wc_format_decimal doesn't return decimal points and trims .00 * Update comment. * Removed unnecessary rounding modes. * Updated comment. * Updated comment. * Updated comment. * Updated Unit Tests. * Lint. * Fix tests. * Re-add rounding modes. * Prevented a fatal if an array is supplied to the method. This was the old behaviour, although it will produce erroneous prices, but before we let this throw a fatal we need to warn devs and track usage. * Added doing_it_wrong() for unexpected types for $value arg. * Early return, removed translation, renamed unit test method. * Added expect notice to unit test. * Add further tests to rounding modes. * Renamed $mock_formatter. This is not a mock. * Fixed tests and added provider for types. * Linting.
This commit is contained in:
parent
64ce7bd732
commit
f3cafa2f17
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fixed a bug that would cause incorrect pricing at checkout for very large amounts.
|
|
@ -8,13 +8,24 @@ namespace Automattic\WooCommerce\StoreApi\Formatters;
|
||||||
*/
|
*/
|
||||||
class MoneyFormatter implements FormatterInterface {
|
class MoneyFormatter implements FormatterInterface {
|
||||||
/**
|
/**
|
||||||
* Format a given value and return the result.
|
* Format a given price value and return the result as a string without decimals.
|
||||||
*
|
*
|
||||||
* @param mixed $value Value to format.
|
* @param int|float|string $value Value to format. Int is allowed, as it may also represent a valid price.
|
||||||
* @param array $options Options that influence the formatting.
|
* @param array $options Options that influence the formatting.
|
||||||
* @return mixed
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function format( $value, array $options = [] ) {
|
public function format( $value, array $options = [] ) {
|
||||||
|
|
||||||
|
if ( ! is_int( $value ) && ! is_string( $value ) && ! is_float( $value ) ) {
|
||||||
|
wc_doing_it_wrong(
|
||||||
|
__FUNCTION__,
|
||||||
|
'Function expects a $value arg of type INT, STRING or FLOAT.',
|
||||||
|
'9.2'
|
||||||
|
);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$options = wp_parse_args(
|
$options = wp_parse_args(
|
||||||
$options,
|
$options,
|
||||||
[
|
[
|
||||||
|
@ -23,12 +34,20 @@ class MoneyFormatter implements FormatterInterface {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (string) intval(
|
// Ensure rounding mode is valid.
|
||||||
round(
|
$rounding_modes = [ PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD ];
|
||||||
( (float) wc_format_decimal( $value ) ) * ( 10 ** absint( $options['decimals'] ) ),
|
$options['rounding_mode'] = absint( $options['rounding_mode'] );
|
||||||
0,
|
if ( ! in_array( $options['rounding_mode'], $rounding_modes, true ) ) {
|
||||||
absint( $options['rounding_mode'] )
|
$options['rounding_mode'] = PHP_ROUND_HALF_UP;
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
$value = floatval( $value );
|
||||||
|
|
||||||
|
// Remove the price decimal points for rounding purposes.
|
||||||
|
$value = $value * pow( 10, absint( $options['decimals'] ) );
|
||||||
|
$value = round( $value, 0, $options['rounding_mode'] );
|
||||||
|
|
||||||
|
// This ensures returning the value as a string without decimal points ready for price parsing.
|
||||||
|
return wc_format_decimal( $value, 0, true );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,10 @@ use Automattic\WooCommerce\StoreApi\Formatters\MoneyFormatter;
|
||||||
*/
|
*/
|
||||||
class TestMoneyFormatter extends \WP_UnitTestCase {
|
class TestMoneyFormatter extends \WP_UnitTestCase {
|
||||||
|
|
||||||
private $mock_formatter;
|
/**
|
||||||
|
* @var MoneyFormatter
|
||||||
|
*/
|
||||||
|
private $formatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup test product data. Called before every test.
|
* Setup test product data. Called before every test.
|
||||||
|
@ -20,30 +23,70 @@ class TestMoneyFormatter extends \WP_UnitTestCase {
|
||||||
protected function setUp(): void {
|
protected function setUp(): void {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->mock_formatter = new MoneyFormatter();
|
$this->formatter = new MoneyFormatter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test formatting.
|
* Test formatting.
|
||||||
*/
|
*/
|
||||||
public function test_format() {
|
public function test_format() {
|
||||||
$this->assertEquals( "1000", $this->mock_formatter->format( 10 ) );
|
$this->assertEquals( '1000', $this->formatter->format( 10 ) );
|
||||||
$this->assertEquals( "1000", $this->mock_formatter->format( "10" ) );
|
$this->assertEquals( '1000', $this->formatter->format( '10' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test formatting with custom DP.
|
* Test formatting with custom DP.
|
||||||
*/
|
*/
|
||||||
public function test_format_dp() {
|
public function test_format_dp() {
|
||||||
$this->assertEquals( "100000", $this->mock_formatter->format( 10, [ 'decimals' => 4 ] ) );
|
$this->assertEquals( '100000', $this->formatter->format( 10, [ 'decimals' => 4 ] ) );
|
||||||
$this->assertEquals( "100000", $this->mock_formatter->format( "10", [ 'decimals' => 4 ] ) );
|
$this->assertEquals( '100000', $this->formatter->format( '10', [ 'decimals' => 4 ] ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test formatting with custom DP.
|
* Test formatting with custom DP.
|
||||||
*/
|
*/
|
||||||
public function test_format_rounding_mode() {
|
public function test_format_rounding_mode() {
|
||||||
$this->assertEquals( "156", $this->mock_formatter->format( 1.555, [ 'rounding_mode' => PHP_ROUND_HALF_UP ] ) );
|
$this->assertEquals( '156', $this->formatter->format( 1.555 ) );
|
||||||
$this->assertEquals( "155", $this->mock_formatter->format( 1.555, [ 'rounding_mode' => PHP_ROUND_HALF_DOWN ] ) );
|
$this->assertEquals( '156', $this->formatter->format( 1.555, [ 'rounding_mode' => PHP_ROUND_HALF_UP ] ) );
|
||||||
|
$this->assertEquals( '155', $this->formatter->format( 1.555, [ 'rounding_mode' => PHP_ROUND_HALF_DOWN ] ) );
|
||||||
|
$this->assertEquals( '156', $this->formatter->format( 1.555, [ 'rounding_mode' => PHP_ROUND_HALF_EVEN ] ) );
|
||||||
|
$this->assertEquals( '155', $this->formatter->format( 1.555, [ 'rounding_mode' => PHP_ROUND_HALF_ODD ] ) );
|
||||||
|
$this->assertEquals( '156', $this->formatter->format( 1.555, [ 'rounding_mode' => 123456 ] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test formatting for int overflow values.
|
||||||
|
*/
|
||||||
|
public function test_format_int_overflow() {
|
||||||
|
$this->assertEquals( '922337203685477580800', $this->formatter->format( '9223372036854775808' ) );
|
||||||
|
$this->assertEquals( '922337203685477580800', $this->formatter->format( floatval( '9223372036854775808' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for invalid param types.
|
||||||
|
*/
|
||||||
|
public function invalidTypesProvider() {
|
||||||
|
return [
|
||||||
|
[ true ],
|
||||||
|
[ null ],
|
||||||
|
[ [ 'Not right' ] ],
|
||||||
|
[ new \StdClass() ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test formatting returns '' if a $value of type INT, STRING or FLOAT is not provided.
|
||||||
|
* @dataProvider invalidTypesProvider
|
||||||
|
*
|
||||||
|
* @param mixed $invalid_type The invalid type to test.
|
||||||
|
*/
|
||||||
|
public function test_format_unexpected_param_types( $invalid_type ) {
|
||||||
|
$this->expected_doing_it_wrong = array_merge(
|
||||||
|
$this->expected_doing_it_wrong,
|
||||||
|
[ 'format' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert that the format method returns an empty string for invalid types.
|
||||||
|
$this->assertEquals( '', $this->formatter->format( $invalid_type ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue