Timesheet validator: prevent overbooking budgets (#2422)

This commit is contained in:
Kevin Papst
2021-03-13 14:41:29 +01:00
committed by GitHub
parent db1344573f
commit 3e9371bd6a
34 changed files with 1425 additions and 236 deletions

View File

@@ -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
);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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()));

View File

@@ -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());

View File

@@ -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()

View File

@@ -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());
}
}

View File

@@ -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());

View 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
],
];
}
}

View 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();
}
}
}