Timesheet validator: prevent overbooking budgets (#2422)
This commit is contained in:
@@ -101,6 +101,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
['name' => 'timesheet.active_entries.default_begin', 'value' => '23:59'],
|
||||
['name' => 'timesheet.rules.allow_future_times', 'value' => false],
|
||||
['name' => 'timesheet.rules.allow_overlapping_records', 'value' => false],
|
||||
['name' => 'timesheet.rules.allow_overbooking_budget', 'value' => false],
|
||||
['name' => 'timesheet.rules.lockdown_period_start', 'value' => null],
|
||||
['name' => 'timesheet.rules.lockdown_period_end', 'value' => null],
|
||||
['name' => 'timesheet.rules.lockdown_grace_period', 'value' => null],
|
||||
@@ -136,6 +137,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
['name' => 'timesheet.active_entries.default_begin', 'value' => '23:59'],
|
||||
['name' => 'timesheet.rules.allow_future_times', 'value' => 1],
|
||||
['name' => 'timesheet.rules.allow_overlapping_records', 'value' => 1],
|
||||
['name' => 'timesheet.rules.allow_overbooking_budget', 'value' => 1],
|
||||
['name' => 'timesheet.rules.lockdown_period_start', 'value' => 'first day of last month'],
|
||||
['name' => 'timesheet.rules.lockdown_period_end', 'value' => 'first day of last month'],
|
||||
['name' => 'timesheet.rules.lockdown_grace_period', 'value' => '+10 days'],
|
||||
@@ -146,8 +148,8 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
],
|
||||
[
|
||||
'#system_configuration_form_timesheet_configuration_0_value', // mode
|
||||
'#system_configuration_form_timesheet_configuration_7_value', // hard_limit
|
||||
'#system_configuration_form_timesheet_configuration_8_value', // soft_limit
|
||||
'#system_configuration_form_timesheet_configuration_8_value', // hard_limit
|
||||
'#system_configuration_form_timesheet_configuration_9_value', // soft_limit
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Configuration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\TimesheetMeta;
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DateRangeType;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use App\Tests\DataFixtures\ActivityFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock;
|
||||
|
||||
@@ -369,6 +373,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
['name' => 'timesheet.active_entries.default_begin', 'value' => '08:00'],
|
||||
['name' => 'timesheet.rules.allow_future_times', 'value' => true],
|
||||
['name' => 'timesheet.rules.allow_overlapping_records', 'value' => false],
|
||||
['name' => 'timesheet.rules.allow_overbooking_budget', 'value' => true],
|
||||
['name' => 'timesheet.rules.lockdown_period_start', 'value' => null],
|
||||
['name' => 'timesheet.rules.lockdown_period_end', 'value' => null],
|
||||
['name' => 'timesheet.rules.lockdown_grace_period', 'value' => null],
|
||||
@@ -406,6 +411,58 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testCreateActionWithOverbookedActivity()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setIsGlobal(true);
|
||||
$fixture->setIsVisible(true);
|
||||
$fixture->setCallback(function (Activity $activity) {
|
||||
$activity->setBudget(1000);
|
||||
$activity->setTimeBudget(3600);
|
||||
});
|
||||
$activities = $this->importFixture($fixture);
|
||||
/** @var Activity $activity */
|
||||
$activity = $activities[0];
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setActivities([$activity]);
|
||||
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
|
||||
$timesheets = $this->importFixture($fixture);
|
||||
$id = $timesheets[0]->getId();
|
||||
|
||||
$this->request($client, '/timesheet/' . $id . '/edit');
|
||||
|
||||
$response = $client->getResponse();
|
||||
$this->assertTrue($response->isSuccessful());
|
||||
|
||||
/** @var ConfigurationRepository $repository */
|
||||
$repository = $this->getEntityManager()->getRepository(Configuration::class);
|
||||
$config = new Configuration();
|
||||
$config->setName('timesheet.rules.allow_overbooking_budget');
|
||||
$config->setValue(false);
|
||||
$repository->saveConfiguration($config);
|
||||
|
||||
$this->assertHasValidationError(
|
||||
$client,
|
||||
'/timesheet/' . $id . '/edit',
|
||||
'form[name=timesheet_edit_form]',
|
||||
[
|
||||
'timesheet_edit_form' => [
|
||||
'hourlyRate' => 100,
|
||||
'begin' => '2020-02-18 01:00',
|
||||
'end' => '2020-02-18 02:10',
|
||||
'project' => 1,
|
||||
'activity' => $activity->getId(),
|
||||
]
|
||||
],
|
||||
['#timesheet_edit_form_activity']
|
||||
);
|
||||
}
|
||||
|
||||
public function testCreateActionWithBeginAndEndAndTagValues()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
@@ -205,6 +205,7 @@ class AppExtensionTest extends TestCase
|
||||
'lockdown_period_start' => null,
|
||||
'lockdown_period_end' => null,
|
||||
'lockdown_grace_period' => null,
|
||||
'allow_overbooking_budget' => true,
|
||||
],
|
||||
'default_begin' => 'now',
|
||||
'duration_increment' => null,
|
||||
|
||||
@@ -285,6 +285,7 @@ class ConfigurationTest extends TestCase
|
||||
'lockdown_period_start' => null,
|
||||
'lockdown_period_end' => null,
|
||||
'lockdown_grace_period' => null,
|
||||
'allow_overbooking_budget' => true,
|
||||
],
|
||||
'duration_increment' => null,
|
||||
'time_increment' => null,
|
||||
|
||||
@@ -38,6 +38,8 @@ class ActivityTest extends TestCase
|
||||
self::assertFalse($sut->hasColor());
|
||||
$this->assertEquals(0.0, $sut->getBudget());
|
||||
$this->assertEquals(0, $sut->getTimeBudget());
|
||||
self::assertFalse($sut->hasBudget());
|
||||
self::assertFalse($sut->hasTimeBudget());
|
||||
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||
$this->assertNull($sut->getMetaField('foo'));
|
||||
@@ -68,9 +70,11 @@ class ActivityTest extends TestCase
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setBudget(12345.67));
|
||||
$this->assertEquals(12345.67, $sut->getBudget());
|
||||
self::assertTrue($sut->hasBudget());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setTimeBudget(937321));
|
||||
$this->assertEquals(937321, $sut->getTimeBudget());
|
||||
self::assertTrue($sut->hasTimeBudget());
|
||||
|
||||
$this->assertTrue($sut->isGlobal());
|
||||
$this->assertInstanceOf(Activity::class, $sut->setProject(new Project()));
|
||||
|
||||
@@ -51,6 +51,8 @@ class CustomerTest extends TestCase
|
||||
self::assertFalse($sut->hasColor());
|
||||
self::assertEquals(0.0, $sut->getBudget());
|
||||
self::assertEquals(0, $sut->getTimeBudget());
|
||||
self::assertFalse($sut->hasBudget());
|
||||
self::assertFalse($sut->hasTimeBudget());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
self::assertEquals(0, $sut->getMetaFields()->count());
|
||||
self::assertNull($sut->getMetaField('foo'));
|
||||
@@ -103,9 +105,11 @@ class CustomerTest extends TestCase
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setBudget(12345.67));
|
||||
self::assertEquals(12345.67, $sut->getBudget());
|
||||
self::assertTrue($sut->hasBudget());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setTimeBudget(937321));
|
||||
self::assertEquals(937321, $sut->getTimeBudget());
|
||||
self::assertTrue($sut->hasTimeBudget());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setVatId('ID 1234567890'));
|
||||
self::assertEquals('ID 1234567890', $sut->getVatId());
|
||||
|
||||
@@ -41,6 +41,8 @@ class ProjectTest extends TestCase
|
||||
self::assertFalse($sut->hasColor());
|
||||
self::assertEquals(0.0, $sut->getBudget());
|
||||
self::assertEquals(0, $sut->getTimeBudget());
|
||||
self::assertFalse($sut->hasBudget());
|
||||
self::assertFalse($sut->hasTimeBudget());
|
||||
self::assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||
self::assertEquals(0, $sut->getMetaFields()->count());
|
||||
self::assertNull($sut->getMetaField('foo'));
|
||||
@@ -95,9 +97,11 @@ class ProjectTest extends TestCase
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setBudget(12345.67));
|
||||
self::assertEquals(12345.67, $sut->getBudget());
|
||||
self::assertTrue($sut->hasBudget());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setTimeBudget(937321));
|
||||
self::assertEquals(937321, $sut->getTimeBudget());
|
||||
self::assertTrue($sut->hasTimeBudget());
|
||||
}
|
||||
|
||||
public function testMetaFields()
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Tests\Model;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Model\ProjectStatistic;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -20,7 +19,7 @@ class ProjectStatisticTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new ProjectStatistic(new Project());
|
||||
$sut = new ProjectStatistic();
|
||||
self::assertEquals(0, $sut->getActivityAmount());
|
||||
self::assertEquals(0, $sut->getRecordAmount());
|
||||
self::assertEquals(0, $sut->getRecordDuration());
|
||||
@@ -28,8 +27,7 @@ class ProjectStatisticTest extends TestCase
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$project = new Project();
|
||||
$sut = new ProjectStatistic($project);
|
||||
$sut = new ProjectStatistic();
|
||||
$sut->setRecordAmount(7654);
|
||||
$sut->setRecordDuration(826);
|
||||
$sut->setActivityAmount(13);
|
||||
@@ -37,6 +35,5 @@ class ProjectStatisticTest extends TestCase
|
||||
self::assertEquals(13, $sut->getActivityAmount());
|
||||
self::assertEquals(7654, $sut->getRecordAmount());
|
||||
self::assertEquals(826, $sut->getRecordDuration());
|
||||
self::assertSame($project, $sut->getProject());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\Calculator\RateCalculator;
|
||||
use App\Timesheet\RateService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@@ -46,7 +47,7 @@ class RateCalculatorTest extends TestCase
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock());
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
||||
$sut->calculate($record);
|
||||
$this->assertEquals(50, $record->getRate());
|
||||
}
|
||||
@@ -62,7 +63,7 @@ class RateCalculatorTest extends TestCase
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock());
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
||||
$sut->calculate($record);
|
||||
$this->assertEquals(10, $record->getRate());
|
||||
}
|
||||
@@ -173,7 +174,7 @@ class RateCalculatorTest extends TestCase
|
||||
$rates[] = $rate;
|
||||
}
|
||||
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock($rates));
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock($rates)));
|
||||
$sut->calculate($timesheet);
|
||||
$this->assertEquals($expectedRate, $timesheet->getRate());
|
||||
$this->assertEquals($expectedInternalRate, $timesheet->getInternalRate());
|
||||
@@ -207,7 +208,7 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
|
||||
$sut = new RateCalculator([], $this->getRateRepositoryMock());
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
||||
$sut->calculate($record);
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
}
|
||||
@@ -233,7 +234,7 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
$record->setEnd($end);
|
||||
|
||||
$sut = new RateCalculator($rules, $this->getRateRepositoryMock());
|
||||
$sut = new RateCalculator(new RateService($rules, $this->getRateRepositoryMock()));
|
||||
$sut->calculate($record);
|
||||
|
||||
$this->assertEquals($expectedRate, $record->getRate());
|
||||
|
||||
283
tests/Timesheet/RateServiceTest.php
Normal file
283
tests/Timesheet/RateServiceTest.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\RateService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Timesheet\RateService
|
||||
*/
|
||||
class RateServiceTest extends TestCase
|
||||
{
|
||||
protected function getRateRepositoryMock(array $rates = [])
|
||||
{
|
||||
$mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
if (!empty($rates)) {
|
||||
$mock->expects($this->any())->method('findMatchingRates')->willReturn($rates);
|
||||
}
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
public function testCalculateWithTimesheetHourlyRate()
|
||||
{
|
||||
$record = new Timesheet();
|
||||
$record->setEnd(new \DateTime());
|
||||
$record->setDuration(1800);
|
||||
$record->setHourlyRate(100);
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
||||
$rate = $sut->calculate($record);
|
||||
$this->assertEquals(50, $rate->getRate());
|
||||
}
|
||||
|
||||
public function testCalculateWithTimesheetFixedRate()
|
||||
{
|
||||
$record = new Timesheet();
|
||||
$record->setEnd(new \DateTime());
|
||||
$record->setDuration(1800);
|
||||
$record->setFixedRate(10);
|
||||
// make sure that fixed rate is always applied, even if hourly rate is set
|
||||
$record->setHourlyRate(99);
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
||||
$rate = $sut->calculate($record);
|
||||
$this->assertEquals(10, $rate->getRate());
|
||||
}
|
||||
|
||||
public function getRateTestData()
|
||||
{ // expected, expInt, durat, userH, userIn, timeH, timeF, actH, actIn, actF, proH, proIn, proFi, custH, custIn, custF
|
||||
yield 'a0' => [0.0, 0.0, 0, 0, 0, null, null, null, null, false, null, null, false, null, null, false];
|
||||
yield 'a2' => [0.0, 0.0, 0, 0, null, null, null, null, null, false, null, null, false, null, null, false];
|
||||
yield 'a4' => [0.0, 0.0, 1800, 0, 0, null, null, null, null, false, null, null, false, null, null, false];
|
||||
yield 'a6' => [0.5, 6.72, 1800, 1, 13.44, null, null, null, null, false, null, null, false, null, null, false];
|
||||
yield 'a8' => [0.0, 1, 0, 0, 0, 0, 0, 0, 1, true, 0, null, true, 0, null, true];
|
||||
// rate: 1.5 => timesheet hourly rate , internal: 2.5 => activity hourly rate (30 min)
|
||||
yield 'b1' => [1.5, 2.5, 1800, 1, 1, 3, null, 5, null, false, 7, null, false, 9, null, false];
|
||||
yield 'b2' => [2.5, 2.5, 1800, 1, 1, null, null, 5, null, false, 7, null, false, 9, null, false];
|
||||
yield 'b3' => [3.5, 6.5, 1800, 1, 1, null, null, null, null, false, 7, 13, false, 9, 9, false];
|
||||
yield 'b4' => [4.5, 6.5, 1800, 1, 15, null, null, null, null, false, null, null, false, 9, 13, false];
|
||||
// rate: 2.0 => timesheet fixed rate , internal: 3.0 => activity fixed rate
|
||||
yield 'b5' => [2.0, 3.0, 1800, 1, 1, null, 2, 3, null, true, 4, null, true, 5, null, true];
|
||||
yield 'b6' => [3.0, 3.0, 1800, 1, 1, null, null, 3, null, true, 4, null, true, 5, null, true];
|
||||
yield 'b7' => [4.0, 4.0, 1800, 1, 1, null, null, null, null, false, 4, null, true, 5, null, true];
|
||||
yield 'b8' => [3.0, 3.0, 1800, 1, 1, null, null, 3, null, true, null, null, false, 5, null, true];
|
||||
// rate: 2.0 => timesheet fixed rate , internal: 5.0 => customer hourly rate
|
||||
yield 'b9' => [2.0, 5.0, 1800, 1, 1, null, 2, null, null, false, null, null, false, 5, null, true];
|
||||
// rate: 5.0 => timesheet hourly rate , internal: 7.5 => user internal rate (30 min)
|
||||
yield 'c0' => [5.0, 7.5, 1800, 100, 15, 10, null, null, null, false, null, null, false, null, null, false];
|
||||
// internal: 10 because no rule applies and as fallback the users internal rate is used
|
||||
yield 'd0' => [10, 100, 1800, 100, 100, null, 10, null, null, false, null, null, false, null, null, false];
|
||||
yield 'e0' => [10, 10, 1800, 100, 100, null, null, 20, null, false, null, null, false, null, null, false];
|
||||
yield 'f0' => [20, 78, 1800, 100, 100, null, null, 20, 78, true, null, null, false, null, null, false];
|
||||
yield 'g0' => [15, 11.5, 1800, 100, 100, null, null, null, null, false, 30, 23, false, null, null, false];
|
||||
yield 'h0' => [30, 30, 1800, 100, 100, null, null, null, null, false, 30, null, true, null, null, false];
|
||||
yield 'i0' => [20, 13.5, 1800, 100, 100, null, null, null, null, false, null, null, false, 40, 27, false];
|
||||
yield 'j0' => [40, 84, 1800, 100, 45, null, null, null, null, false, null, null, false, 40, 84, true];
|
||||
// make sure the last fallback for the internal rate is the users hourly rate
|
||||
yield 'k0' => [8.82, 6, 1800, 17.64, 12, null, null, null, null, false, null, null, false, null, null, true];
|
||||
yield 'k1' => [8.82, 8.82, 1800, 17.64, null, null, null, null, null, false, null, null, false, null, null, true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getRateTestData
|
||||
*/
|
||||
public function testRates(
|
||||
$expectedRate,
|
||||
$expectedInternalRate,
|
||||
$duration,
|
||||
$userRate,
|
||||
$userInternalRate,
|
||||
$timesheetHourly,
|
||||
$timesheetFixed,
|
||||
$activityRate,
|
||||
$activityInternal,
|
||||
$activityIsFixed,
|
||||
$projectRate,
|
||||
$projectInternal,
|
||||
$projectIsFixed,
|
||||
$customerRate,
|
||||
$customerInternal,
|
||||
$customerIsFixed
|
||||
) {
|
||||
$customer = new Customer();
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setProject($project);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
->setEnd(new \DateTime())
|
||||
->setHourlyRate($timesheetHourly)
|
||||
->setFixedRate($timesheetFixed)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setDuration($duration)
|
||||
->setUser($this->getTestUser($userRate, $userInternalRate))
|
||||
;
|
||||
|
||||
$rates = [];
|
||||
|
||||
if (null !== $customerRate) {
|
||||
$rate = new CustomerRate();
|
||||
$rate->setRate($customerRate);
|
||||
$rate->setIsFixed($customerIsFixed);
|
||||
if (null !== $customerInternal) {
|
||||
$rate->setInternalRate($customerInternal);
|
||||
}
|
||||
$rates[] = $rate;
|
||||
}
|
||||
|
||||
if (null !== $projectRate) {
|
||||
$rate = new ProjectRate();
|
||||
$rate->setRate($projectRate);
|
||||
$rate->setIsFixed($projectIsFixed);
|
||||
if (null !== $projectInternal) {
|
||||
$rate->setInternalRate($projectInternal);
|
||||
}
|
||||
$rates[] = $rate;
|
||||
}
|
||||
|
||||
if (null !== $activityRate) {
|
||||
$rate = new ActivityRate();
|
||||
$rate->setRate($activityRate);
|
||||
$rate->setIsFixed($activityIsFixed);
|
||||
if (null !== $activityInternal) {
|
||||
$rate->setInternalRate($activityInternal);
|
||||
}
|
||||
$rates[] = $rate;
|
||||
}
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock($rates));
|
||||
$rate = $sut->calculate($timesheet);
|
||||
$this->assertEquals($expectedRate, $rate->getRate());
|
||||
$this->assertEquals($expectedInternalRate, $rate->getInternalRate());
|
||||
}
|
||||
|
||||
protected function getTestUser($rate = 75, $internalRate = 75)
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$pref = new UserPreference();
|
||||
$pref->setName(UserPreference::HOURLY_RATE);
|
||||
$pref->setValue($rate);
|
||||
|
||||
$prefInt = new UserPreference();
|
||||
$prefInt->setName(UserPreference::INTERNAL_RATE);
|
||||
$prefInt->setValue($internalRate);
|
||||
|
||||
$user->setPreferences([$pref, $prefInt]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function testCalculateWithEmptyEnd()
|
||||
{
|
||||
$record = new Timesheet();
|
||||
$record->setBegin(new \DateTime());
|
||||
$record->setDuration(1800);
|
||||
$record->setFixedRate(100);
|
||||
$record->setHourlyRate(100);
|
||||
$record->setActivity(new Activity());
|
||||
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
||||
$rate = $sut->calculate($record);
|
||||
$this->assertEquals(0, $rate->getRate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the hourly rate from user_preferences to calculate the rate.
|
||||
*
|
||||
* @dataProvider getRuleDefinitions
|
||||
*/
|
||||
public function testCalculateWithRulesByUsersHourlyRate($duration, $rules, $expectedRate)
|
||||
{
|
||||
$end = new \DateTime('12:00:00', new \DateTimeZone('UTC'));
|
||||
$start = clone $end;
|
||||
$start->setTimestamp($end->getTimestamp() - $duration);
|
||||
|
||||
$record = new Timesheet();
|
||||
$record->setUser($this->getTestUser());
|
||||
$record->setBegin($start);
|
||||
$record->setDuration($duration);
|
||||
$record->setActivity(new Activity());
|
||||
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
|
||||
$record->setEnd($end);
|
||||
|
||||
$sut = new RateService($rules, $this->getRateRepositoryMock());
|
||||
$rate = $sut->calculate($record);
|
||||
|
||||
$this->assertEquals($expectedRate, $rate->getRate());
|
||||
}
|
||||
|
||||
public function getRuleDefinitions()
|
||||
{
|
||||
$start = new \DateTime('12:00:00', new \DateTimeZone('UTC'));
|
||||
$day = $start->format('l');
|
||||
|
||||
return [
|
||||
[
|
||||
31837,
|
||||
[],
|
||||
663.27
|
||||
],
|
||||
[
|
||||
31837,
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
'factor' => 2.0
|
||||
],
|
||||
'foo' => [
|
||||
'days' => ['bar'],
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
1326.54
|
||||
],
|
||||
[
|
||||
31837,
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
'factor' => 2.0
|
||||
],
|
||||
'foo' => [
|
||||
'days' => ['MonDay', 'tUEsdAy', 'WEdnesday', 'THursday', 'friDay', 'SATURday', 'sunDAY'],
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
2321.45
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
390
tests/Validator/TimesheetBudgetUsedValidatorTest.php
Normal file
390
tests/Validator/TimesheetBudgetUsedValidatorTest.php
Normal file
@@ -0,0 +1,390 @@
|
||||
<?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\Validator;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\ActivityStatistic;
|
||||
use App\Model\CustomerStatistic;
|
||||
use App\Model\ProjectStatistic;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\Rate;
|
||||
use App\Timesheet\RateService;
|
||||
use App\Timesheet\RateServiceInterface;
|
||||
use App\Validator\Constraints\TimesheetBudgetUsedConstraint;
|
||||
use App\Validator\TimesheetBudgetUsedValidator;
|
||||
use DateTime;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Validator\Constraints\TimesheetBudgetUsedConstraint
|
||||
* @covers \App\Validator\TimesheetBudgetUsedValidator
|
||||
*/
|
||||
class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
protected function createValidator(bool $isAllowed = false, ?ActivityStatistic $activityStatistic = null, ?ProjectStatistic $projectStatistic = null, ?CustomerStatistic $customerStatistic = null, ?array $rawData = null, ?Rate $rate = null)
|
||||
{
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
$configuration->method('isTimesheetAllowOverbookingBudget')->willReturn($isAllowed);
|
||||
|
||||
$customerRepository = $this->createMock(CustomerRepository::class);
|
||||
$customerStatistic = $customerStatistic ?? new CustomerStatistic();
|
||||
$customerRepository->method('getCustomerStatistics')->willReturn($customerStatistic);
|
||||
|
||||
$projectRepository = $this->createMock(ProjectRepository::class);
|
||||
$projectStatistic = $projectStatistic ?? new ProjectStatistic();
|
||||
$projectRepository->method('getProjectStatistics')->willReturn($projectStatistic);
|
||||
|
||||
$activityRepository = $this->createMock(ActivityRepository::class);
|
||||
$activityStatistic = $activityStatistic ?? new ActivityStatistic();
|
||||
$activityRepository->method('getActivityStatistics')->willReturn($activityStatistic);
|
||||
|
||||
$timesheetRepository = $this->createMock(TimesheetRepository::class);
|
||||
if (null !== $rawData) {
|
||||
$timesheetRepository->method('getRawData')->willReturn($rawData);
|
||||
}
|
||||
|
||||
if ($rate !== null) {
|
||||
$rateService = $this->createMock(RateServiceInterface::class);
|
||||
$rateService->method('calculate')->willReturn($rate);
|
||||
} else {
|
||||
$rateService = new RateService([], $timesheetRepository);
|
||||
}
|
||||
|
||||
return new TimesheetBudgetUsedValidator($configuration, $customerRepository, $projectRepository, $activityRepository, $timesheetRepository, $rateService);
|
||||
}
|
||||
|
||||
public function testConstraintIsInvalid()
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new Timesheet(), new NotBlank());
|
||||
}
|
||||
|
||||
public function testConstraintWithPreExistingViolation()
|
||||
{
|
||||
$this->validator = $this->createValidator();
|
||||
$this->validator->initialize($this->context);
|
||||
$this->context->addViolation('FOOOOOOOOO');
|
||||
|
||||
$this->validator->validate(new Timesheet(), new TimesheetBudgetUsedConstraint());
|
||||
$this->buildViolation('FOOOOOOOOO')->assertRaised();
|
||||
}
|
||||
|
||||
public function testTargetIsInvalid()
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate('foo', new TimesheetBudgetUsedConstraint());
|
||||
}
|
||||
|
||||
public function testWithMissingEnd()
|
||||
{
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin(new DateTime());
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetBudgetUsedConstraint());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWithMissingUser()
|
||||
{
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin(new DateTime());
|
||||
$timesheet->setEnd(new DateTime());
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetBudgetUsedConstraint());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWithMissingProject()
|
||||
{
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin(new DateTime());
|
||||
$timesheet->setEnd(new DateTime());
|
||||
$timesheet->setUser(new User());
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetBudgetUsedConstraint());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWithoutBudget()
|
||||
{
|
||||
$project = new Project();
|
||||
$project->setCustomer(new Customer());
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin(new DateTime());
|
||||
$timesheet->setEnd(new DateTime());
|
||||
$timesheet->setUser(new User());
|
||||
$timesheet->setProject($project);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetBudgetUsedConstraint());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWithAllowedOverbooking()
|
||||
{
|
||||
$this->validator = $this->createValidator(true);
|
||||
$this->validator->initialize($this->context);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setTimeBudget(3600);
|
||||
|
||||
$begin = new DateTime();
|
||||
$end = clone $begin;
|
||||
$end->modify('+3601 seconds');
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer(new Customer());
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
$timesheet->setUser(new User());
|
||||
$timesheet->setProject($project);
|
||||
$timesheet->setActivity($activity);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetBudgetUsedConstraint());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function getViolationTestData()
|
||||
{
|
||||
return [
|
||||
// activity: violations
|
||||
'a_a' => [1230, null, null, null, null, null, 3600, null, null, null, null, null, '00:20', '00:39', '01:00', 'activity', '+3600 seconds'],
|
||||
'a_b' => [null, 1001.0, null, null, null, null, null, 1000.0, null, null, null, null, '€1,001.00', '€0.00', '€1,000.00', 'activity', '+3600 seconds'],
|
||||
|
||||
// activity: no violations
|
||||
'a_c' => [1230, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'a_d' => [null, 1001.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'a_e' => [1230, 1001.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
|
||||
// previously logged available budgets expected violation duration entry currently in database
|
||||
'a_f' => [1320, null, null, null, null, null, 3600, null, null, null, null, null, '00:22', '00:38', '01:00', 'activity', '+3600 seconds', ['rate' => 1.0, 'duration' => 1000]],
|
||||
'a_h1' => [7200, null, null, null, null, null, 7200, null, null, null, null, null, '02:00', '00:00', '02:00', 'activity', '+3601 seconds', ['rate' => 1.0, 'duration' => 3600]],
|
||||
'a_h' => [3601, null, null, null, null, null, 3600, null, null, null, null, null, null, null, null, null, '+3600 seconds', ['rate' => 1.0, 'duration' => 3601]],
|
||||
'a_g' => [null, 1002.0, null, null, null, null, null, 1000.0, null, null, null, null, '1,002.00', '0.00', '1,000.00', 'activity', '+3600 seconds', ['rate' => 1.0, 'duration' => 1010]],
|
||||
'a_g1' => [null, 1002.0, null, null, null, null, null, 1000.0, null, null, null, null, null, null, null, null, '+3600 seconds', ['rate' => 2.0, 'duration' => 0]],
|
||||
// nothing changed => no violation
|
||||
'a_x1' => [3600, 1000.0, null, null, null, null, 3600, 1000.0, null, null, null, null, null, null, null, null, '+3600 seconds', ['rate' => 1000.0, 'duration' => 3600], new Rate(1000.0, 0.00)],
|
||||
|
||||
// project: violations
|
||||
'p_j' => [null, null, 1230, null, null, null, null, null, 3600, null, null, null, '00:20', '00:39', '01:00', 'project', '+3600 seconds'],
|
||||
'p_k' => [null, null, null, 1001.0, null, null, null, null, null, 1000.0, null, null, '€1,001.00', '€0.00', '€1,000.00', 'project', '+3600 seconds'],
|
||||
|
||||
// previously logged available budgets expected violation duration entry currently in database
|
||||
'p_f' => [null, null, 1320, null, null, null, null, null, 3600, null, null, null, '00:22', '00:38', '01:00', 'project', '+3600 seconds', ['rate' => 1.0, 'duration' => 1000]],
|
||||
'p_h1' => [null, null, 7200, null, null, null, null, null, 7200, null, null, null, '02:00', '00:00', '02:00', 'project', '+3601 seconds', ['rate' => 1.0, 'duration' => 3600]],
|
||||
'p_h' => [null, null, 3601, null, null, null, null, null, 3600, null, null, null, null, null, null, null, '+3600 seconds', ['rate' => 1.0, 'duration' => 3601]],
|
||||
'p_g' => [null, null, null, 1002.0, null, null, null, null, null, 1000.0, null, null, '1,002.00', '0.00', '1,000.00', 'project', '+3600 seconds', ['rate' => 1.0, 'duration' => 1010]],
|
||||
'p_g1' => [null, null, null, 1002.0, null, null, null, null, null, 1000.0, null, null, null, null, null, null, '+3600 seconds', ['rate' => 2.0, 'duration' => 0]],
|
||||
|
||||
// project: no violations
|
||||
'p_n' => [null, null, 1230, null, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'p_o' => [null, null, null, 1001.0, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'p_p' => [null, null, 1230, 1001, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
|
||||
'p_q' => [1230, null, 1230, null, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'p_r' => [1230, 1001.0, 1230, null, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'p_s' => [null, 1001.0, null, 1001.0, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'p_t' => [null, 1001.0, 1230, 1001.0, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'p_u' => [1230, 1001.0, 1230, 1001.0, null, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
|
||||
// customer: violations
|
||||
'c_v' => [null, null, null, null, 1230, null, null, null, null, null, 3600, null, '00:20', '00:39', '01:00', 'customer', '+3600 seconds'],
|
||||
'c_w' => [null, null, null, null, null, 1001.0, null, null, null, null, null, 1000.0, '€1,001.00', '€0.00', '€1,000.00', 'customer', '+3600 seconds'],
|
||||
|
||||
// previously logged available budgets expected violation duration entry currently in database
|
||||
'c_f' => [null, null, null, null, 1320, null, null, null, null, null, 3600, null, '00:22', '00:38', '01:00', 'customer', '+3600 seconds', ['rate' => 1.0, 'duration' => 1000]],
|
||||
'c_h1' => [null, null, null, null, 7200, null, null, null, null, null, 7200, null, '02:00', '00:00', '02:00', 'customer', '+3601 seconds', ['rate' => 1.0, 'duration' => 3600]],
|
||||
'c_h' => [null, null, null, null, 3601, null, null, null, null, null, 3600, null, null, null, null, null, '+3600 seconds', ['rate' => 1.0, 'duration' => 3601]],
|
||||
'c_g' => [null, null, null, null, null, 1002.0, null, null, null, null, null, 1000.0, '1,002.00', '0.00', '1,000.00', 'customer', '+3600 seconds', ['rate' => 1.0, 'duration' => 1010]],
|
||||
'c_g1' => [null, null, null, null, null, 1002.0, null, null, null, null, null, 1000.0, null, null, null, null, '+3600 seconds', ['rate' => 2.0, 'duration' => 0]],
|
||||
|
||||
// customer: no violations
|
||||
'c_z' => [null, null, null, null, 1230, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_1' => [null, null, null, null, null, 1001.0, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_2' => [null, null, null, null, 1230, 1001.0, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_3' => [1230, null, 1230, null, 1230, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_4' => [1230, 1001.0, 1230, null, null, 1001.0, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_5' => [null, 1001.0, null, 1001.0, 1230, 1001.0, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_6' => [null, 1001.0, 1230, 1001.0, 1230, null, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
'c_7' => [1230, 1001.0, 1230, 1001.0, null, 1001.0, null, null, null, null, null, null, null, null, null, null, '+3600 seconds'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getViolationTestData
|
||||
*/
|
||||
public function testWithActivityTimeBudget(
|
||||
?int $activityDuration,
|
||||
?float $activityRate,
|
||||
?int $projectDuration,
|
||||
?float $projectRate,
|
||||
?int $customerDuration,
|
||||
?float $customerRate,
|
||||
?int $activityTimeBudget,
|
||||
?float $activityBudget,
|
||||
?int $projectTimeBudget,
|
||||
?float $projectBudget,
|
||||
?int $customerTimeBudget,
|
||||
?float $customerBudget,
|
||||
?string $used,
|
||||
?string $free,
|
||||
?string $budget,
|
||||
?string $path,
|
||||
string $duration,
|
||||
array $rawData = [],
|
||||
?Rate $rate = null
|
||||
) {
|
||||
$activityStatistic = new ActivityStatistic();
|
||||
if ($activityDuration !== null) {
|
||||
$activityStatistic->setRecordDuration($activityDuration);
|
||||
}
|
||||
if ($activityRate !== null) {
|
||||
$activityStatistic->setRecordRate($activityRate);
|
||||
}
|
||||
|
||||
$projectStatistic = new ProjectStatistic();
|
||||
if ($projectDuration !== null) {
|
||||
$projectStatistic->setRecordDuration($projectDuration);
|
||||
}
|
||||
if ($projectRate !== null) {
|
||||
$projectStatistic->setRecordRate($projectRate);
|
||||
}
|
||||
|
||||
$customerStatistic = new CustomerStatistic();
|
||||
if ($customerDuration !== null) {
|
||||
$customerStatistic->setRecordDuration($customerDuration);
|
||||
}
|
||||
if ($customerRate !== null) {
|
||||
$customerStatistic->setRecordRate($customerRate);
|
||||
}
|
||||
|
||||
$begin = new DateTime();
|
||||
$end = clone $begin;
|
||||
$end->modify($duration);
|
||||
|
||||
if (!empty($rawData)) {
|
||||
if (!\array_key_exists('activity', $rawData)) {
|
||||
$rawData['activity'] = 1;
|
||||
}
|
||||
if (!\array_key_exists('project', $rawData)) {
|
||||
$rawData['project'] = 1;
|
||||
}
|
||||
if (!\array_key_exists('customer', $rawData)) {
|
||||
$rawData['customer'] = 1;
|
||||
}
|
||||
$activity = $this->createMock(Activity::class);
|
||||
$activity->method('getId')->willReturn($rawData['activity']);
|
||||
if ($activityTimeBudget !== null) {
|
||||
$activity->method('getTimeBudget')->willReturn($activityTimeBudget);
|
||||
$activity->method('hasTimeBudget')->willReturn(true);
|
||||
}
|
||||
if ($activityBudget !== null) {
|
||||
$activity->method('getBudget')->willReturn($activityBudget);
|
||||
$activity->method('hasBudget')->willReturn(true);
|
||||
}
|
||||
|
||||
$customer = $this->createMock(Customer::class);
|
||||
$customer->method('getId')->willReturn($rawData['customer']);
|
||||
if ($customerTimeBudget !== null) {
|
||||
$customer->method('getTimeBudget')->willReturn($customerTimeBudget);
|
||||
$customer->method('hasTimeBudget')->willReturn(true);
|
||||
}
|
||||
if ($customerBudget !== null) {
|
||||
$customer->method('getBudget')->willReturn($customerBudget);
|
||||
$customer->method('hasBudget')->willReturn(true);
|
||||
}
|
||||
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn($rawData['project']);
|
||||
$project->method('getCustomer')->willReturn($customer);
|
||||
if ($projectTimeBudget !== null) {
|
||||
$project->method('getTimeBudget')->willReturn($projectTimeBudget);
|
||||
$project->method('hasTimeBudget')->willReturn(true);
|
||||
}
|
||||
if ($projectBudget !== null) {
|
||||
$project->method('getBudget')->willReturn($projectBudget);
|
||||
$project->method('hasBudget')->willReturn(true);
|
||||
}
|
||||
|
||||
$timesheet = $this->createMock(Timesheet::class);
|
||||
$timesheet->method('getId')->willReturn(1);
|
||||
$timesheet->method('getRate')->willReturn($rawData['rate']);
|
||||
$timesheet->method('getBegin')->willReturn($begin);
|
||||
$timesheet->method('getEnd')->willReturn($end);
|
||||
$timesheet->method('getUser')->willReturn(new User());
|
||||
$timesheet->method('getProject')->willReturn($project);
|
||||
$timesheet->method('getActivity')->willReturn($activity);
|
||||
} else {
|
||||
$activity = new Activity();
|
||||
if ($activityTimeBudget !== null) {
|
||||
$activity->setTimeBudget($activityTimeBudget);
|
||||
}
|
||||
if ($activityBudget !== null) {
|
||||
$activity->setBudget($activityBudget);
|
||||
}
|
||||
|
||||
$customer = new Customer();
|
||||
if ($customerTimeBudget !== null) {
|
||||
$customer->setTimeBudget($customerTimeBudget);
|
||||
}
|
||||
if ($customerBudget !== null) {
|
||||
$customer->setBudget($customerBudget);
|
||||
}
|
||||
|
||||
$project = new Project();
|
||||
if ($projectTimeBudget !== null) {
|
||||
$project->setTimeBudget($projectTimeBudget);
|
||||
}
|
||||
if ($projectBudget !== null) {
|
||||
$project->setBudget($projectBudget);
|
||||
}
|
||||
$project->setCustomer($customer);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
$timesheet->setUser(new User());
|
||||
$timesheet->setProject($project);
|
||||
$timesheet->setActivity($activity);
|
||||
}
|
||||
|
||||
$this->validator = $this->createValidator(false, $activityStatistic, $projectStatistic, $customerStatistic, $rawData, $rate);
|
||||
$this->validator->initialize($this->context);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetBudgetUsedConstraint());
|
||||
|
||||
if (null === $used && null === $budget && null === $free && $path === null) {
|
||||
$this->assertNoViolation();
|
||||
} else {
|
||||
$this->buildViolation('The budget is completely used.')
|
||||
->atPath('property.path.' . $path)
|
||||
->setParameters([
|
||||
'%used%' => $used,
|
||||
'%budget%' => $budget,
|
||||
'%free%' => $free
|
||||
])
|
||||
->assertRaised();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user