From 88eefa698bab8993708e7585c2624b2c0a3f5231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 7 Feb 2023 15:09:40 +0100 Subject: [PATCH] Fixed: add string key for summary of charges and resources into doc generation --- .../Repository/ChargeKindRepository.php | 11 +- .../ChargeKindRepositoryInterface.php | 49 ++++++ .../Repository/ResourceKindRepository.php | 11 +- .../ResourceKindRepositoryInterface.php | 49 ++++++ .../Repository/ResourceRepository.php | 2 +- .../Service/Summary/SummaryBudget.php | 36 ++-- .../Service/Summary/SummaryBudgetTest.php | 155 ++++++++++++++++++ .../ParticipationOverlapValidatorTest.php | 62 +++++++ 8 files changed, 349 insertions(+), 26 deletions(-) create mode 100644 src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php create mode 100644 src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php create mode 100644 src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Validator/AccompanyingPeriod/ParticipationOverlapValidatorTest.php diff --git a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php index e170a362a..10d02749a 100644 --- a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php +++ b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php @@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository; use Chill\BudgetBundle\Entity\ChargeKind; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -class ChargeKindRepository implements ObjectRepository +final class ChargeKindRepository implements ChargeKindRepositoryInterface { private EntityRepository $repository; @@ -50,7 +49,8 @@ class ChargeKindRepository implements ObjectRepository ->where($qb->expr()->eq('c.isActive', 'true')) ->orderBy('c.ordering', 'ASC') ->getQuery() - ->getResult(); + ->getResult() + ; } /** @@ -77,6 +77,11 @@ class ChargeKindRepository implements ObjectRepository return $this->repository->findOneBy($criteria); } + public function findOneByKind(string $kind): ?ChargeKind + { + return $this->repository->findOneBy(['kind' => $kind]); + } + public function getClassName(): string { return ChargeKind::class; diff --git a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php new file mode 100644 index 000000000..5099a5674 --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php @@ -0,0 +1,49 @@ +where($qb->expr()->eq('r.isActive', 'true')) ->orderBy('r.ordering', 'ASC') ->getQuery() - ->getResult(); + ->getResult() + ; } /** @@ -77,6 +77,11 @@ class ResourceKindRepository implements ObjectRepository return $this->repository->findOneBy($criteria); } + public function findOneByKind(string $kind): ?ResourceKind + { + return $this->repository->findOneBy(['kind' => $kind]); + } + public function getClassName(): string { return ResourceKind::class; diff --git a/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php b/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php new file mode 100644 index 000000000..658a87a3d --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php @@ -0,0 +1,49 @@ +andWhere('c.startDate < :date') // TODO: there is a misconception here, the end date must be lower or null. startDate are never null //->andWhere('c.startDate < :date OR c.startDate IS NULL'); -; + ; if (null !== $sort) { $qb->orderBy($sort); diff --git a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php index 096531c52..b7a88bb9d 100644 --- a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php +++ b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php @@ -14,7 +14,9 @@ namespace Chill\BudgetBundle\Service\Summary; use Chill\BudgetBundle\Entity\ChargeKind; use Chill\BudgetBundle\Entity\ResourceKind; use Chill\BudgetBundle\Repository\ChargeKindRepository; +use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface; use Chill\BudgetBundle\Repository\ResourceKindRepository; +use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Person; @@ -27,7 +29,7 @@ use function count; /** * Helps to find a summary of the budget: the sum of resources and charges. */ -class SummaryBudget implements SummaryBudgetInterface +final class SummaryBudget implements SummaryBudgetInterface { private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id'; @@ -37,23 +39,19 @@ class SummaryBudget implements SummaryBudgetInterface private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id'; - private ChargeKindRepository $chargeKindRepository; - - private array $chargeLabels; + private ChargeKindRepositoryInterface $chargeKindRepository; private EntityManagerInterface $em; - private ResourceKindRepository $resourceKindRepository; - - private array $resourcesLabels; + private ResourceKindRepositoryInterface $resourceKindRepository; private TranslatableStringHelperInterface $translatableStringHelper; public function __construct( EntityManagerInterface $em, TranslatableStringHelperInterface $translatableStringHelper, - ResourceKindRepository $resourceKindRepository, - ChargeKindRepository $chargeKindRepository + ResourceKindRepositoryInterface $resourceKindRepository, + ChargeKindRepositoryInterface $chargeKindRepository ) { $this->em = $em; $this->translatableStringHelper = $translatableStringHelper; @@ -129,19 +127,19 @@ class SummaryBudget implements SummaryBudgetInterface private function getEmptyChargeArray(): array { - $keys = array_map(static fn (ChargeKind $kind) => $kind->getId(), $this->chargeKindRepository->findAll()); + $keys = array_map(static fn (ChargeKind $kind) => $kind->getKind(), $this->chargeKindRepository->findAll()); - return array_combine($keys, array_map(function ($id) { - return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->find($id)->getName()), 'comment' => '']; + return array_combine($keys, array_map(function ($kind) { + return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->findOneByKind($kind)->getName()), 'comment' => '']; }, $keys)); } private function getEmptyResourceArray(): array { - $keys = array_map(static fn (ResourceKind $kind) => $kind->getId(), $this->resourceKindRepository->findAll()); + $keys = array_map(static fn (ResourceKind $kind) => $kind->getKind(), $this->resourceKindRepository->findAll()); - return array_combine($keys, array_map(function ($id) { - return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->find($id)->getName()), 'comment' => '']; + return array_combine($keys, array_map(function ($kind) { + return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->findOneByKind($kind)->getName()), 'comment' => '']; }, $keys)); } @@ -152,10 +150,10 @@ class SummaryBudget implements SummaryBudgetInterface switch ($kind) { case 'charge': foreach ($rows as $row) { - $chargeKind = $this->chargeKindRepository->find($row['kind_id']); + $chargeKind = $this->chargeKindRepository->findOneByKind($row['kind_id']); if (null === $chargeKind) { - throw new RuntimeException('charge kind not found'); + throw new RuntimeException('charge kind not found: ' . $row['kind_id']); } $result[$chargeKind->getKind()] = [ 'sum' => (float) $row['sum'], @@ -168,10 +166,10 @@ class SummaryBudget implements SummaryBudgetInterface case 'resource': foreach ($rows as $row) { - $resourceKind = $this->resourceKindRepository->find($row['kind_id']); + $resourceKind = $this->resourceKindRepository->findOneByKind($row['kind_id']); if (null === $resourceKind) { - throw new RuntimeException('charge kind not found'); + throw new RuntimeException('charge kind not found: ' . $row['kind_id']); } $result[$resourceKind->getKind()] = [ diff --git a/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php b/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php new file mode 100644 index 000000000..9f90c60bd --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php @@ -0,0 +1,155 @@ +prophesize(AbstractQuery::class); + $queryCharges->getResult()->willReturn([ + [ + 'sum' => 250.0, + 'comment' => '', + 'kind_id' => 'rental', + ], + ]); + $queryCharges->setParameters(Argument::type('array')) + ->will(function ($args, $query) { + return $query; + }) + ; + + $queryResources = $this->prophesize(AbstractQuery::class); + $queryResources->getResult()->willReturn([ + [ + 'sum' => 1500.0, + 'comment' => '', + 'kind_id' => 'salary', + ], + ]); + $queryResources->setParameters(Argument::type('array')) + ->will(function ($args, $query) { + return $query; + }) + ; + + $em = $this->prophesize(EntityManagerInterface::class); + $em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class)) + ->will(function ($args) use ($queryResources, $queryCharges) { + if (false !== strpos($args[0], 'chill_budget.resource')) { + return $queryResources->reveal(); + } + if (false !== strpos($args[0], 'chill_budget.charge')) { + return $queryCharges->reveal(); + } + throw new \RuntimeException('this query does not have a stub counterpart: '.$args[0]); + }) + ; + + $chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class); + $chargeRepository->findAll()->willReturn([ + $rental = (new ChargeKind())->setKind('rental')->setName(['fr' => 'Rental']), + $other = (new ChargeKind())->setKind('other')->setName(['fr' => 'Other']), + ]); + $chargeRepository->findOneByKind('rental')->willReturn($rental); + $chargeRepository->findOneByKind('other')->willReturn($other); + + $resourceRepository = $this->prophesize(ResourceKindRepositoryInterface::class); + $resourceRepository->findAll()->willReturn([ + $salary = (new ResourceKind())->setKind('salary')->setName(['fr' => 'Salary']), + $misc = (new ResourceKind())->setKind('misc')->setName(['fr' => 'Misc']), + ]); + $resourceRepository->findOneByKind('salary')->willReturn($salary); + $resourceRepository->findOneByKind('misc')->willReturn($misc); + + $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelper->localize(Argument::type('array'))->will(function ($arg) { + return $arg[0]['fr']; + }); + + $person = new Person(); + $personReflection = new \ReflectionClass($person); + $personIdReflection = $personReflection->getProperty('id'); + $personIdReflection->setAccessible(true); + $personIdReflection->setValue($person, 1); + + $household = new Household(); + $householdReflection = new \ReflectionClass($household); + $householdReflection->getProperty('id')->setAccessible(true); + $householdReflection->getProperty('id')->setValue($household, 1); + $householdMember = (new HouseholdMember())->setPerson($person) + ->setStartDate(new \DateTimeImmutable('1 month ago')) + ; + $household->addMember($householdMember); + + $summaryBudget = new SummaryBudget( + $em->reveal(), + $translatableStringHelper->reveal(), + $resourceRepository->reveal(), + $chargeRepository->reveal() + ); + + $summary = $summaryBudget->getSummaryForPerson($person); + $summaryForHousehold = $summaryBudget->getSummaryForHousehold($household); + + // we check the structure for the summary. The structure is the same for household + // and persons + + $expected = [ + 'charges' => [ + 'rental' => ['sum' => 250.0, 'comment' => '', 'label' => 'Rental'], + 'other' => ['sum' => 0.0, 'comment' => '', 'label' => 'Other'], + ], + 'resources' => [ + 'salary' => ['sum' => 1500.0, 'comment' => '', 'label' => 'Salary'], + 'misc' => ['sum' => 0.0, 'comment' => '', 'label' => 'Misc'], + ], + ]; + + foreach ([$summaryForHousehold, $summary] as $summary) { + $this->assertIsArray($summary); + $this->assertEqualsCanonicalizing(['charges', 'resources'], array_keys($summary)); + $this->assertEqualsCanonicalizing(['rental', 'other'], array_keys($summary['charges'])); + $this->assertEqualsCanonicalizing(['salary', 'misc'], array_keys($summary['resources'])); + + foreach ($expected as $resCha => $contains) { + foreach ($contains as $kind => $row) { + $this->assertEqualsCanonicalizing($row, $summary[$resCha][$kind]); + } + } + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Validator/AccompanyingPeriod/ParticipationOverlapValidatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Validator/AccompanyingPeriod/ParticipationOverlapValidatorTest.php new file mode 100644 index 000000000..181834b6a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Validator/AccompanyingPeriod/ParticipationOverlapValidatorTest.php @@ -0,0 +1,62 @@ +prophesize(PersonRenderInterface::class); + $personRender->renderString(Argument::is(Person::class), [])->willReturn('person'); + $thirdPartyRender = $this->prophesize(ThirdPartyRender::class); + $thirdPartyRender->renderString(Argument::is(ThirdParty::class), [])->willReturn('thirdparty'); + + return new ParticipationOverlapValidator($personRender->reveal(), $thirdPartyRender->reveal()); + } + + public function testOneParticipation() + { + $period = new AccompanyingPeriod(); + $person = new Person(); + + $collection = new ArrayCollection([ + new AccompanyingPeriodParticipation($period, $person) + ]); + + $this->validator->validate($collection, $this->getConstraint()); + + $this->assertNoViolation(); + } + + /** + * @return mixed + */ + public function getConstraint() + { + return new ParticipationOverlap(); + } +}