diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php index 996c195cc..d2142f43f 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php @@ -55,7 +55,11 @@ class HouseholdMemberController extends ApiController $em = $this->getDoctrine()->getManager(); // if new household, persist it - if (FALSE === $em->contains($editor->getHousehold())) { + if ( + $editor->hasHousehold() + && + FALSE === $em->contains($editor->getHousehold()) + ) { $em->persist($editor->getHousehold()); } diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php index 970010288..bc25c725f 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php @@ -3,6 +3,7 @@ namespace Chill\PersonBundle\Household; use Symfony\Component\Validator\ConstraintViolationListInterface; +use Doctrine\Common\Collections\Criteria; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Household\Position; use Chill\PersonBundle\Entity\Household\Household; @@ -13,12 +14,12 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; class MembersEditor { private ValidatorInterface $validator; - private Household $household; + private ?Household $household = null; private array $persistables = []; private array $membershipsAffected = []; - public function __construct(ValidatorInterface $validator, Household $household) + public function __construct(ValidatorInterface $validator, ?Household $household) { $this->validation = $validator; $this->household = $household; @@ -62,6 +63,33 @@ class MembersEditor return $this; } + public function leaveMovement( + \DateTimeImmutable $date, + Person $person + ): self { + $criteria = new Criteria(); + $expr = Criteria::expr(); + + $criteria->where( + $expr->andX( + $expr->lt('startDate', $date), + $expr->isNull('endDate', $date) + ) + ); + + $participations = $person->getHouseholdParticipations() + ->matching($criteria) + ; + + foreach ($participations as $participation) { + $participation->setEndDate($date); + $this->membershipsAffected[] = $participation; + } + + return $this; + } + + public function validate(): ConstraintViolationListInterface { @@ -72,8 +100,13 @@ class MembersEditor return $this->persistables; } - public function getHousehold(): Household + public function getHousehold(): ?Household { return $this->household; } + + public function hasHousehold(): bool + { + return $this->household !== null; + } } diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php b/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php index 611308b19..adf55277e 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php @@ -14,7 +14,7 @@ class MembersEditorFactory $this->validator = $validator; } - public function createEditor(Household $household): MembersEditor + public function createEditor(?Household $household = null): MembersEditor { return new MembersEditor($this->validator, $household); } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php index 12fe4ab5c..57745e276 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php @@ -24,6 +24,58 @@ class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwar } public function denormalize($data, string $type, string $format = null, array $context = []) + { + // some test about schema first... + $this->performChecks($data); + + // route to "leave movement" (all concerned leave household) + // or "move to another household" (all concerned go to another + // household) + if (NULL === $data['destination']) { + return $this->denormalizeLeave($data, $type, $format, $context); + } else { + return $this->denormalizeMove($data, $type, $format, $context); + } + } + + private function performChecks($data): void + { + if (NULL == $data['concerned'] ?? NULL + && FALSE === ·\is_array('concerned')) { + throw new Exception\UnexpectedValueException("The schema does not have any key 'concerned'"); + } + + if (FALSE === \array_key_exists('destination', $data)) { + throw new Exception\UnexpectedValueException("The schema does not have any key 'destination'"); + } + } + + protected function denormalizeLeave($data, string $type, string $format, array $context = []) + { + $editor = $this->factory->createEditor(null); + + foreach ($data['concerned'] as $key => $concerned) { + $person = $this->denormalizer->denormalize($concerned['person'] ?? null, Person::class, + $format, $context); + $startDate = $this->denormalizer->denormalize($concerned['start_date'] ?? null, \DateTimeImmutable::class, + $format, $context); + + if ( + NULL === $person + && NULL === $startDate + ) { + throw new Exception\InvalidArgumentException("position with ". + "key $key could not be denormalized: missing ". + "person or start_date."); + } + + $editor->leaveMovement($startDate, $person); + } + + return $editor; + } + + protected function denormalizeMove($data, string $type, string $format, array $context = []) { $household = $this->denormalizer->denormalize($data['destination'], Household::class, $format, $context); @@ -34,11 +86,6 @@ class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwar $editor = $this->factory->createEditor($household); - if (NULL == $data['concerned'] ?? NULL - && FALSE === ·\is_array('concerned')) { - throw new Exception\UnexpectedValueException("The schema does not have any key 'concerned'"); - } - foreach ($data['concerned'] as $key => $concerned) { $person = $this->denormalizer->denormalize($concerned['person'] ?? null, Person::class, $format, $context); @@ -62,9 +109,9 @@ class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwar $editor->addMovement($startDate, $person, $position, $holder, $comment); - - return $editor; } + + return $editor; } public function supportsDenormalization($data, string $type, string $format = null) diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php index 97c08f88d..fcaa4ae34 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php @@ -67,6 +67,116 @@ class HouseholdMemberControllerTest extends WebTestCase ); } + /** + * @dataProvider provideValidDataMove + */ + public function testMoveMemberToNewHousehold($personId, $householdId, $positionId, \DateTimeInterface $date) + { + $client = $this->getClientAuthenticated(); + + $client->request( + Request::METHOD_POST, + '/api/1.0/person/household/members/move.json', + [], // parameters + [], // files + [], // server + \json_encode( + [ + 'concerned' => + [ + [ + 'person' => + [ + 'type' => 'person', + 'id' => $personId + ], + 'start_date' => + [ + 'datetime' => $date->format(\DateTimeInterface::RFC3339) + ], + 'position' => + [ + 'type' => 'household_position', + 'id' => $positionId + ], + 'holder' => false, + 'comment' => "Introduced by automated test", + ], + ], + 'destination' => + [ + 'type' => 'household', + ] + ], + true) + ); + + $this->assertEquals(Response::HTTP_OK, + $client->getResponse()->getStatusCode() + ); + + $data = \json_decode($client->getResponse()->getContent(), true); + + $this->assertIsArray($data); + $this->assertArrayHasKey('members', $data); + $this->assertIsArray($data['members']); + $this->assertEquals(1, count($data['members']), + "assert new household count one member"); + $this->assertArrayHasKey('person', $data['members'][0]); + $this->assertArrayHasKey('id', $data['members'][0]['person']); + $this->assertEquals($personId, $data['members'][0]['person']['id']); + } + + /** + * @dataProvider provideValidDataMove + */ + public function testLeaveWithoutHousehold($personId, $householdId, $positionId, \DateTimeInterface $date) + { + $client = $this->getClientAuthenticated(); + + $client->request( + Request::METHOD_POST, + '/api/1.0/person/household/members/move.json', + [], // parameters + [], // files + [], // server + \json_encode( + [ + 'concerned' => + [ + [ + 'person' => + [ + 'type' => 'person', + 'id' => $personId + ], + 'start_date' => + [ + 'datetime' => $date->format(\DateTimeInterface::RFC3339) + ], + 'position' => + [ + 'type' => 'household_position', + 'id' => $positionId + ], + 'holder' => false, + 'comment' => "Introduced by automated test", + ], + ], + 'destination' => null + ], + true) + ); + + $this->assertEquals(Response::HTTP_OK, + $client->getResponse()->getStatusCode() + ); + + $data = \json_decode($client->getResponse()->getContent(), true); + + $this->assertEquals(null, $data); + } + /** * @dataProvider provideValidDataEditMember */ @@ -97,7 +207,13 @@ class HouseholdMemberControllerTest extends WebTestCase $em = self::$container->get(EntityManagerInterface::class); $personIds = $em->createQuery("SELECT p.id FROM ".Person::class." p ". - "JOIN p.center c WHERE c.name = :center") + "JOIN p.center c ". + "JOIN p.householdParticipations hp ". + "WHERE ". + "c.name = :center ". + "AND hp.startDate < CURRENT_DATE() ". + "AND hp.endDate IS NULL " + ) ->setParameter('center', "Center A") ->setMaxResults(100) ->getScalarResult() diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index 30178e385..bd3a8706f 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -802,18 +802,61 @@ paths: type: object properties: person: - $ref: '#/components/schemas/PersonById' + $ref: '#/components/schemas/PersonById' start_date: - $ref: '#/components/schemas/Date' + $ref: '#/components/schemas/Date' position: - $ref: '#/components/schemas/HouseholdPosition' + $ref: '#/components/schemas/HouseholdPosition' holder: type: boolean comment: type: string destination: - oneOf: - - $ref: '#/components/schemas/Household' + $ref: '#/components/schemas/Household' + examples: + Moving person to a new household: + value: + concerned: + - + person: + id: 0 + type: person + position: + type: position + id: 1 + start_date: + datetime: 2021-06-01T00:00:00+02:00 + comment: "This is my comment for moving" + holder: false + destination: + type: household + Moving person to an existing household: + value: + concerned: + - + person: + id: 0 + type: person + position: + type: position + id: 1 + start_date: + datetime: 2021-06-01T00:00:00+02:00 + comment: "This is my comment for moving" + holder: false + destination: + type: household + id: 54 + Removing a person from any household: + value: + concerned: + - + person: + id: 0 + type: person + start_date: + datetime: 2021-06-01T00:00:00+02:00 + destination: null responses: 401: description: "Unauthorized"