Configurable rate rounding (#5734)
* added invoice hydration of the issuer object * added "rate calculator" mode * new config to select rounding mode * replace static calls with dependency injection
This commit is contained in:
130
tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php
Normal file
130
tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\RateCalculator;
|
||||
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(ClassicRateCalculator::class)]
|
||||
class ClassicRateCalculatorTest extends TestCase
|
||||
{
|
||||
#[DataProvider('provideRates')]
|
||||
public function testCalculateRate(float $hourlyRate, int $seconds, float $expected): void
|
||||
{
|
||||
$sut = new ClassicRateCalculator();
|
||||
|
||||
$result = $sut->calculateRate($hourlyRate, $seconds);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function provideRates(): array
|
||||
{
|
||||
return [
|
||||
'zero duration' => [100.0, 0, 0.0],
|
||||
'full hour' => [100.0, 3600, 100.0],
|
||||
'half hour' => [100.0, 1800, 50.0],
|
||||
'one minute with rounding' => [123.4567, 60, 2.0576],
|
||||
'one second tiny amount' => [1.23, 1, 0.0003],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRoundDurationKeepsOriginalSeconds(): void
|
||||
{
|
||||
$sut = new ClassicRateCalculator();
|
||||
|
||||
$this->assertSame(0, $sut->roundDuration(0));
|
||||
$this->assertSame(59, $sut->roundDuration(59));
|
||||
$this->assertSame(3601, $sut->roundDuration(3601));
|
||||
}
|
||||
|
||||
#[DataProvider('getRateCalculationData')]
|
||||
public function testCalculateRates(float $hourlyRate, int $duration, float $expectedRate): void
|
||||
{
|
||||
$sut = new ClassicRateCalculator();
|
||||
self::assertEquals($expectedRate, $sut->calculateRate($hourlyRate, $duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<float, int, >>|\Generator
|
||||
*/
|
||||
public static function getRateCalculationData()
|
||||
{
|
||||
yield [0, 0, 0];
|
||||
yield [1, 100, 0.0278];
|
||||
yield [1, 900, 0.25];
|
||||
yield [1, 1800, 0.5];
|
||||
yield [10000, 1, 2.7778];
|
||||
yield [736, 123, 25.1467];
|
||||
yield [7360, 1234, 2522.8444];
|
||||
yield [7360.34, 1234, 2522.961];
|
||||
yield [7360.01, 1234, 2522.8479];
|
||||
yield [7360.99, 1234, 2523.1838];
|
||||
}
|
||||
|
||||
public function testCalculateRateWithRounding(): void
|
||||
{
|
||||
$total = 0.00;
|
||||
$seconds = 0;
|
||||
$repeat = 130;
|
||||
$sut = new ClassicRateCalculator();
|
||||
|
||||
for ($a = 0; $a < $repeat; $a++) {
|
||||
$inputs = [
|
||||
900,
|
||||
1600,
|
||||
4200,
|
||||
8763,
|
||||
3300,
|
||||
600,
|
||||
1300,
|
||||
1837,
|
||||
4217,
|
||||
5400,
|
||||
3283,
|
||||
600,
|
||||
];
|
||||
|
||||
foreach ($inputs as $i) {
|
||||
$seconds += $i;
|
||||
$total += $sut->calculateRate(114.75, $i);
|
||||
}
|
||||
}
|
||||
|
||||
self::assertEquals(36000 * $repeat, $seconds);
|
||||
self::assertEquals(1147.50 * $repeat, $total);
|
||||
}
|
||||
|
||||
public function testDecimalDuration(): void
|
||||
{
|
||||
$inputs = [
|
||||
900,
|
||||
1600,
|
||||
4200,
|
||||
8763,
|
||||
3300,
|
||||
600,
|
||||
1300,
|
||||
1837,
|
||||
4217,
|
||||
5400,
|
||||
3283,
|
||||
600,
|
||||
7200,
|
||||
];
|
||||
|
||||
$sut = new ClassicRateCalculator();
|
||||
foreach ($inputs as $row) {
|
||||
self::assertEquals($row, $sut->roundDuration($row));
|
||||
}
|
||||
}
|
||||
}
|
||||
142
tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php
Normal file
142
tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\RateCalculator;
|
||||
|
||||
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DecimalRateCalculator::class)]
|
||||
class DecimalRateCalculatorTest extends TestCase
|
||||
{
|
||||
#[DataProvider('provideRates')]
|
||||
public function testCalculateRate(float $hourlyRate, int $seconds, float $expected): void
|
||||
{
|
||||
$sut = new DecimalRateCalculator();
|
||||
|
||||
$result = $sut->calculateRate($hourlyRate, $seconds);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function provideRates(): array
|
||||
{
|
||||
return [
|
||||
'zero duration' => [100.0, 0, 0.0],
|
||||
'full hour' => [100.0, 3600, 100.0],
|
||||
'half hour' => [100.0, 1800, 50.0],
|
||||
'one minute with rounding' => [123.4567, 60, 2.47],
|
||||
'one second tiny amount' => [1.23, 1, 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRoundDurationKeepsOriginalSeconds(): void
|
||||
{
|
||||
$sut = new DecimalRateCalculator();
|
||||
|
||||
$this->assertSame(0, $sut->roundDuration(0));
|
||||
$this->assertSame(36, $sut->roundDuration(45));
|
||||
$this->assertSame(72, $sut->roundDuration(59));
|
||||
$this->assertSame(1224, $sut->roundDuration(1234));
|
||||
$this->assertSame(3600, $sut->roundDuration(3601));
|
||||
}
|
||||
|
||||
#[DataProvider('getRateCalculationData')]
|
||||
public function testCalculateRates(float $hourlyRate, int $duration, float $expectedRate): void
|
||||
{
|
||||
$sut = new DecimalRateCalculator();
|
||||
self::assertEquals($expectedRate, $sut->calculateRate($hourlyRate, $duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<float, int, >>|\Generator
|
||||
*/
|
||||
public static function getRateCalculationData()
|
||||
{
|
||||
yield [0.00, 0, 0.00];
|
||||
yield [10.00, 7260, 20.2];
|
||||
yield [1.00, 3600, 1.00];
|
||||
yield [1.00, 100, 0.03];
|
||||
yield [1.00, 900, 0.25];
|
||||
yield [1.00, 1800, 0.5];
|
||||
yield [10000.00, 60, 200.00];
|
||||
yield [736.00, 123, 22.08];
|
||||
yield [7360.00, 1234, 2502.4];
|
||||
yield [7360.34, 1234, 2502.52];
|
||||
yield [7360.01, 1234, 2502.4];
|
||||
yield [7360.99, 1234, 2502.74];
|
||||
}
|
||||
|
||||
public function testCalculateRateWithRounding(): void
|
||||
{
|
||||
$total = 0.00;
|
||||
$seconds = 0;
|
||||
$repeat = 130;
|
||||
|
||||
$inputs = [
|
||||
[900, 28.69, 0],
|
||||
[1600, 50.49, 0],
|
||||
[4200, 134.26, 0],
|
||||
[8763, 278.84, 0],
|
||||
[3300, 105.57, 0],
|
||||
[600, 19.51, 0],
|
||||
[1300, 41.31, 0],
|
||||
[1837, 58.52, 0],
|
||||
[4217, 134.26, 0],
|
||||
[5400, 172.13, 0],
|
||||
[3283, 104.42, 0],
|
||||
[600, 19.51, 0],
|
||||
];
|
||||
|
||||
$totalExpected = 0.00;
|
||||
$sut = new DecimalRateCalculator();
|
||||
|
||||
for ($a = 0; $a < $repeat; $a++) {
|
||||
foreach ($inputs as $row) {
|
||||
[$duration, $rate] = $row;
|
||||
$seconds += $duration;
|
||||
$totalExpected += $rate;
|
||||
$tmp = $sut->calculateRate(114.75, $duration);
|
||||
self::assertEquals($rate, $tmp);
|
||||
$total += $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
self::assertEquals(36000 * $repeat, $seconds);
|
||||
self::assertEquals($totalExpected, $total);
|
||||
self::assertEqualsWithDelta(1147.51 * $repeat, $total, 0.00001);
|
||||
self::assertEqualsWithDelta(149176.3, $total, 0.00001);
|
||||
}
|
||||
|
||||
public function testDecimalDuration(): void
|
||||
{
|
||||
$inputs = [
|
||||
[900, 900],
|
||||
[1600, 1584],
|
||||
[4200, 4212],
|
||||
[8763, 8748],
|
||||
[3300, 3312],
|
||||
[600, 612],
|
||||
[1300, 1296],
|
||||
[1837, 1836],
|
||||
[4217, 4212],
|
||||
[5400, 5400],
|
||||
[3283, 3276],
|
||||
[600, 612],
|
||||
[7200, 7200],
|
||||
];
|
||||
|
||||
$sut = new DecimalRateCalculator();
|
||||
foreach ($inputs as $row) {
|
||||
self::assertEquals($row[1], $sut->roundDuration($row[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
48
tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php
Normal file
48
tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet\RateCalculator;
|
||||
|
||||
use App\Tests\Mocks\SystemConfigurationFactory;
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorFactory;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(RateCalculatorFactory::class)]
|
||||
class RateCalculatorFactoryTest extends TestCase
|
||||
{
|
||||
private function assertCreatesClassicConfig(array $config): void
|
||||
{
|
||||
$config = SystemConfigurationFactory::createStub($config);
|
||||
$sut = new RateCalculatorFactory($config);
|
||||
|
||||
$mode = $sut->getRateCalculatorMode();
|
||||
self::assertInstanceOf(ClassicRateCalculator::class, $mode);
|
||||
}
|
||||
|
||||
public function testCreatesClassic(): void
|
||||
{
|
||||
$this->assertCreatesClassicConfig([]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => 'classic']]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => '']]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => 'foo']]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => 'DECIMAL']]);
|
||||
}
|
||||
|
||||
public function testCreateWithDecimalConfig(): void
|
||||
{
|
||||
$config = SystemConfigurationFactory::createStub(['invoice' => ['rounding_mode' => 'decimal']]);
|
||||
$sut = new RateCalculatorFactory($config);
|
||||
|
||||
$mode = $sut->getRateCalculatorMode();
|
||||
self::assertInstanceOf(DecimalRateCalculator::class, $mode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user