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

@@ -10,12 +10,9 @@
namespace App\Timesheet\Calculator;
use App\Entity\Rate;
use App\Entity\RateInterface;
use App\Entity\Timesheet;
use App\Entity\UserPreference;
use App\Repository\TimesheetRepository;
use App\Timesheet\CalculatorInterface;
use App\Timesheet\Util;
use App\Timesheet\RateService;
/**
* Implementation to calculate the rate for a timesheet record.
@@ -23,136 +20,28 @@ use App\Timesheet\Util;
class RateCalculator implements CalculatorInterface
{
/**
* @var array
* @var RateService
*/
private $rates;
/**
* @var TimesheetRepository
*/
private $repository;
private $service;
public function __construct(array $rates, TimesheetRepository $repository)
public function __construct(RateService $service)
{
$this->rates = $rates;
$this->repository = $repository;
$this->service = $service;
}
/**
* @param Timesheet $record
*/
public function calculate(Timesheet $record)
{
if (null === $record->getEnd()) {
$record->setRate(0);
$record->setInternalRate(0);
$rate = $this->service->calculate($record);
return;
$record->setRate($rate->getRate());
$record->setInternalRate($rate->getInternalRate());
if ($rate->getHourlyRate() !== null) {
$record->setHourlyRate($rate->getHourlyRate());
}
$fixedRate = $record->getFixedRate();
$hourlyRate = $record->getHourlyRate();
$fixedInternalRate = null;
$internalRate = null;
$rate = $this->getBestFittingRate($record);
if (null !== $rate) {
if ($rate->isFixed()) {
$fixedRate = $fixedRate ?? $rate->getRate();
$fixedInternalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) {
$fixedInternalRate = $rate->getInternalRate();
}
} else {
$hourlyRate = $hourlyRate ?? $rate->getRate();
$internalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) {
$internalRate = $rate->getInternalRate();
}
}
if ($rate->getFixedRate() !== null) {
$record->setFixedRate($rate->getFixedRate());
}
if (null !== $fixedRate) {
$record->setFixedRate($fixedRate);
$record->setRate($fixedRate);
if (null === $fixedInternalRate) {
$fixedInternalRate = (float) $record->getUser()->getPreferenceValue(UserPreference::INTERNAL_RATE, $fixedRate);
}
$record->setInternalRate($fixedInternalRate);
return;
}
// user preferences => fallback if nothing else was configured
if (null === $hourlyRate) {
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0.00);
}
if (null === $internalRate) {
$internalRate = $record->getUser()->getPreferenceValue(UserPreference::INTERNAL_RATE, 0.00);
if (null === $internalRate) {
$internalRate = $hourlyRate;
} else {
$internalRate = (float) $internalRate;
}
}
$factor = $this->getRateFactor($record);
$factoredHourlyRate = (float) ($hourlyRate * $factor);
$factoredInternalRate = (float) ($internalRate * $factor);
$totalRate = 0;
$totalInternalRate = 0;
if (null !== $record->getDuration()) {
$totalRate = Util::calculateRate($factoredHourlyRate, $record->getDuration());
$totalInternalRate = Util::calculateRate($factoredInternalRate, $record->getDuration());
}
$record->setHourlyRate($factoredHourlyRate);
$record->setInternalRate($totalInternalRate);
$record->setRate($totalRate);
}
private function getBestFittingRate(Timesheet $timesheet): ?RateInterface
{
$rates = $this->repository->findMatchingRates($timesheet);
/** @var RateInterface[] $sorted */
$sorted = [];
foreach ($rates as $rate) {
$score = $rate->getScore();
if (null !== $rate->getUser() && $timesheet->getUser() === $rate->getUser()) {
++$score;
}
$sorted[$score] = $rate;
}
if (!empty($sorted)) {
ksort($sorted);
return end($sorted);
}
return null;
}
/**
* @param Timesheet $record
* @return float
*/
protected function getRateFactor(Timesheet $record)
{
$factor = 0;
foreach ($this->rates as $rateFactor) {
$weekday = $record->getEnd()->format('l');
$days = array_map('strtolower', $rateFactor['days']);
if (\in_array(strtolower($weekday), $days)) {
$factor += $rateFactor['factor'];
}
}
if ($factor <= 0) {
$factor = 1;
}
return $factor;
}
}

58
src/Timesheet/Rate.php Normal file
View File

@@ -0,0 +1,58 @@
<?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\Timesheet;
final class Rate
{
/**
* @var float
*/
private $rate = 0.00;
/**
* @var float
*/
private $internalRate = 0.00;
/**
* @var float|null
*/
private $fixedRate = null;
/**
* @var float|null
*/
private $hourlyRate = null;
public function __construct(float $rate, float $internalRate, ?float $hourlyRate = null, ?float $fixedRate = null)
{
$this->rate = $rate;
$this->internalRate = $internalRate;
$this->fixedRate = $fixedRate;
$this->hourlyRate = $hourlyRate;
}
public function getRate(): float
{
return $this->rate;
}
public function getInternalRate(): float
{
return $this->internalRate;
}
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
}

View File

@@ -0,0 +1,144 @@
<?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\Timesheet;
use App\Entity\RateInterface;
use App\Entity\Timesheet;
use App\Entity\UserPreference;
use App\Repository\TimesheetRepository;
/**
* Implementation to calculate the rate for a timesheet record.
*/
final class RateService implements RateServiceInterface
{
/**
* @var array
*/
private $rates;
/**
* @var TimesheetRepository
*/
private $repository;
public function __construct(array $rates, TimesheetRepository $repository)
{
$this->rates = $rates;
$this->repository = $repository;
}
public function calculate(Timesheet $record): Rate
{
if (null === $record->getEnd()) {
return new Rate(0.00, 0.00);
}
$fixedRate = $record->getFixedRate();
$hourlyRate = $record->getHourlyRate();
$fixedInternalRate = null;
$internalRate = null;
$rate = $this->getBestFittingRate($record);
if (null !== $rate) {
if ($rate->isFixed()) {
$fixedRate = $fixedRate ?? $rate->getRate();
$fixedInternalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) {
$fixedInternalRate = $rate->getInternalRate();
}
} else {
$hourlyRate = $hourlyRate ?? $rate->getRate();
$internalRate = $rate->getRate();
if (null !== $rate->getInternalRate()) {
$internalRate = $rate->getInternalRate();
}
}
}
if (null !== $fixedRate) {
if (null === $fixedInternalRate) {
$fixedInternalRate = (float) $record->getUser()->getPreferenceValue(UserPreference::INTERNAL_RATE, $fixedRate);
}
return new Rate($fixedRate, $fixedInternalRate, null, $fixedRate);
}
// user preferences => fallback if nothing else was configured
if (null === $hourlyRate) {
$hourlyRate = (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0.00);
}
if (null === $internalRate) {
$internalRate = $record->getUser()->getPreferenceValue(UserPreference::INTERNAL_RATE, 0.00);
if (null === $internalRate) {
$internalRate = $hourlyRate;
} else {
$internalRate = (float) $internalRate;
}
}
$factor = $this->getRateFactor($record);
$factoredHourlyRate = (float) ($hourlyRate * $factor);
$factoredInternalRate = (float) ($internalRate * $factor);
$totalRate = 0;
$totalInternalRate = 0;
if (null !== $record->getDuration()) {
$totalRate = Util::calculateRate($factoredHourlyRate, $record->getDuration());
$totalInternalRate = Util::calculateRate($factoredInternalRate, $record->getDuration());
}
return new Rate($totalRate, $totalInternalRate, $factoredHourlyRate, null);
}
private function getBestFittingRate(Timesheet $timesheet): ?RateInterface
{
$rates = $this->repository->findMatchingRates($timesheet);
/** @var RateInterface[] $sorted */
$sorted = [];
foreach ($rates as $rate) {
$score = $rate->getScore();
if (null !== $rate->getUser() && $timesheet->getUser() === $rate->getUser()) {
++$score;
}
$sorted[$score] = $rate;
}
if (!empty($sorted)) {
ksort($sorted);
return end($sorted);
}
return null;
}
private function getRateFactor(Timesheet $record): float
{
$factor = 0.00;
foreach ($this->rates as $rateFactor) {
$weekday = $record->getEnd()->format('l');
$days = array_map('strtolower', $rateFactor['days']);
if (\in_array(strtolower($weekday), $days)) {
$factor += $rateFactor['factor'];
}
}
if ($factor <= 0) {
$factor = 1.00;
}
return (float) $factor;
}
}

View File

@@ -0,0 +1,20 @@
<?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\Timesheet;
use App\Entity\Timesheet;
/**
* Implementation to calculate the rate for a timesheet record.
*/
interface RateServiceInterface
{
public function calculate(Timesheet $record): Rate;
}