diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php new file mode 100644 index 000000000..c6fd4dd59 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php @@ -0,0 +1,50 @@ +getSerializer() + ->deserialize($request->getContent(), MembersEditor::class, + $_format, ['groups' => [ "read" ]]); + } catch (Exception\InvalidArgumentException | Exception\UnexpectedValueException $e) { + throw new BadRequestException("Deserialization error: {$e->getMessage()}", 45896, $e); + } + dump($editor); + // TODO ACL + // + // TODO validation + // + $em = $this->getDoctrine()->getManager(); + + // to ensure closing membership before creating one, we must manually open a transaction + $em->beginTransaction(); + + foreach ($editor->getPersistable() as $el) { + $em->persist($el); + } + + $em->flush(); + $em->commit(); + + + return $this->json($editor->getHousehold(), Response::HTTP_OK, ["groups" => ["read"]]); + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php index 420325d6d..ac22b6e8b 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php @@ -5,12 +5,16 @@ namespace Chill\PersonBundle\Entity\Household; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\Collection; use Chill\MainBundle\Entity\Address; +use Symfony\Component\Serializer\Annotation as Serializer; /** * @ORM\Entity * @ORM\Table( * name="chill_person_household" * ) + * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ + * "household"=Household::class + * }) */ class Household { @@ -18,6 +22,7 @@ class Household * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") + * @Serializer\Groups({"read"}) */ private $id; diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php index 89a9f3299..52bd969fc 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php @@ -29,12 +29,12 @@ class HouseholdMember private ?Position $position = null; /** - * @ORM\Column(type="date", nullable=true, options={"default": null}) + * @ORM\Column(type="date_immutable", nullable=true, options={"default": null}) */ private ?\DateTimeImmutable $startDate = null; /** - * @ORM\Column(type="date", nullable= true, options={"default": null}) + * @ORM\Column(type="date_immutable", nullable= true, options={"default": null}) */ private ?\DateTimeImmutable $endDate = null; @@ -95,24 +95,24 @@ class HouseholdMember return $this; } - public function getStartDate(): ?\DateTimeInterface + public function getStartDate(): ?\DateTimeImmutable { return $this->startDate; } - public function setStartDate(\DateTimeInterface $startDate): self + public function setStartDate(\DateTimeImmutable $startDate): self { $this->startDate = $startDate; return $this; } - public function getEndDate(): ?\DateTimeInterface + public function getEndDate(): ?\DateTimeImmutable { return $this->endDate; } - public function setEndDate(\DateTimeInterface $endDate): self + public function setEndDate(\DateTimeImmutable $endDate): self { $this->endDate = $endDate; @@ -150,8 +150,7 @@ class HouseholdMember } $this->person = $person; - - $person->addHouseholdParticipation($this); + $this->person->addHouseholdParticipation($this); return $this; } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php index 070399661..156631b63 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php @@ -4,10 +4,14 @@ namespace Chill\PersonBundle\Entity\Household; use Chill\PersonBundle\Repository\Household\PositionRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation as Serializer; /** * @ORM\Entity(repositoryClass=PositionRepository::class) * @ORM\Table(name="chill_person_household_position") + * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ + * "household_position"=Position::class + * }) */ class Position { @@ -15,6 +19,7 @@ class Position * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") + * @Serializer\Groups({ "read" }) */ private $id; diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php index 8dfd11b7b..527404c75 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php @@ -16,7 +16,7 @@ class MembersEditor private Household $household; private array $persistables = []; - private array $memershipsAffected = []; + private array $membershipsAffected = []; public function __construct(ValidatorInterface $validator, Household $household) { @@ -24,7 +24,7 @@ class MembersEditor $this->household = $household; } - public function addMovement(\DateTimeInterface $date, Person $person, Position $position, ?bool $holder = false, ?string $comment = null): self + public function addMovement(\DateTimeImmutable $date, Person $person, Position $position, ?bool $holder = false, ?string $comment = null): self { if (NULL === $this->household) { throw new \LogicException("You must define a household first"); @@ -71,4 +71,9 @@ class MembersEditor { return $this->persistables; } + + public function getHousehold(): Household + { + return $this->household; + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php index 0ca9ef10e..6d07f791e 100644 --- a/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php @@ -2,7 +2,7 @@ namespace Chill\PersonBundle\Repository\Household; -use App\Entity\Chill\PersonBundle\Entity\Household\Position; +use Chill\PersonBundle\Entity\Household\Position; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -19,32 +19,4 @@ class PositionRepository extends ServiceEntityRepository parent::__construct($registry, Position::class); } - // /** - // * @return Position[] Returns an array of Position objects - // */ - /* - public function findByExampleField($value) - { - return $this->createQueryBuilder('p') - ->andWhere('p.exampleField = :val') - ->setParameter('val', $value) - ->orderBy('p.id', 'ASC') - ->setMaxResults(10) - ->getQuery() - ->getResult() - ; - } - */ - - /* - public function findOneBySomeField($value): ?Position - { - return $this->createQueryBuilder('p') - ->andWhere('p.exampleField = :val') - ->setParameter('val', $value) - ->getQuery() - ->getOneOrNullResult() - ; - } - */ } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php new file mode 100644 index 000000000..603c1a9e5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php @@ -0,0 +1,75 @@ +factory = $factory; + } + + public function denormalize($data, string $type, string $format = null, array $context = []) + { + $household = $this->denormalizer->denormalize($data['destination'], Household::class, + $format, $context); + + if (NULL === $household) { + throw new Exception\InvalidArgumentException("household could not be denormalized. Impossible to process"); + } + + $editor = $this->factory->createEditor($household); + + if (NULL == $data['concerned'] ?? [] + && 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); + $position = $this->denormalizer->denormalize($concerned['position'] ?? null, Position::class, + $format, $context); + $startDate = $this->denormalizer->denormalize($concerned['start_date'] ?? null, \DateTimeImmutable::class, + $format, $context); + + $holder = (bool) $concerned['holder'] ?? false; + $comment = (string) $concerned['comment'] ?? false; + + if ( + NULL === $person + && NULL === $position + && NULL === $startDate + ) { + throw new Exception\InvalidArgumentException("position with ". + "key $key could not be denormalized: missing ". + "person, position or start_date."); + } + + $editor->addMovement($startDate, $person, $position, $holder, + $comment); + + return $editor; + } + } + + public function supportsDenormalization($data, string $type, string $format = null) + { + return $type === MembersEditor::class; + } + +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php new file mode 100644 index 000000000..96cb81382 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php @@ -0,0 +1,98 @@ +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', + 'id' => $householdId + ] + ], + true) + ); + + $this->assertEquals(Response::HTTP_OK, + $client->getResponse()->getStatusCode() + ); + } + + public function provideValidData(): \Iterator + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $personIds = $em->createQuery("SELECT p.id FROM ".Person::class." p ". + "JOIN p.center c WHERE c.name = :center") + ->setParameter('center', "Center A") + ->setMaxResults(100) + ->getScalarResult() + ; + \shuffle($personIds); + + $household = new Household(); + $em->persist($household); + $em->flush(); + + $positions = $em->createQuery("SELECT pos.id FROM ".Position::class." pos ". + "WHERE pos.shareHouseHold = TRUE") + ->getResult() + ; + + yield [ + \array_pop($personIds)['id'], + $household->getId(), + $positions[\random_int(0, count($positions) - 1)]['id'], + new \DateTimeImmutable('today') + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdMemberTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdMemberTest.php new file mode 100644 index 000000000..f18a62781 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdMemberTest.php @@ -0,0 +1,34 @@ +setShareHousehold(true) + ; + $membership = (new HouseholdMember()) + ->setPosition($position) + ; + + $this->assertTrue($membership->getShareHousehold()); + } + + public function testPositionDoNotSharehousehold() + { + $position = (new Position()) + ->setShareHousehold(false) + ; + $membership = (new HouseholdMember()) + ->setPosition($position) + ; + + $this->assertFalse($membership->getShareHousehold()); + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php b/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php index ee3e2df86..8ef409296 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php @@ -37,10 +37,10 @@ class MembersEditorTest extends TestCase $person, $position); - $this->assertInstanceOf(Collection::class, $person->getHouseholdParticipations()); - $this->assertEquals(1, $person->getHouseholdParticipations()->count()); + $persistables = $editor->getPersistable(); + $this->assertEquals(\count($persistables), 1); - $membership1 = $person->getHouseholdParticipations()->first(); + $membership1 = $persistables[0]; $this->assertSame($household1, $membership1->getHousehold()); $this->assertNull($membership1->getEndDate()); @@ -52,9 +52,10 @@ class MembersEditorTest extends TestCase $person, $position); - $this->assertEquals(2, $person->getHouseholdParticipations()->count()); + $persistables = $editor->getPersistable(); + $this->assertEquals(1, count($persistables)); - $membership2 = $person->getHouseholdParticipations()->last(); + $membership2 = $persistables[0]; $this->assertSame($household2, $membership2->getHousehold()); $this->assertNull($membership2->getEndDate()); $this->assertNotNull($membership1->getEndDate(), @@ -77,8 +78,8 @@ class MembersEditorTest extends TestCase $person, $position); - $this->assertInstanceOf(Collection::class, $person->getHouseholdParticipations()); - $this->assertEquals(1, $person->getHouseholdParticipations()->count()); + $persistables = $editor->getPersistable(); + $this->assertEquals(1, count($persistables)); $membership1 = $person->getHouseholdParticipations()->first(); $this->assertSame($household1, $membership1->getHousehold()); @@ -92,7 +93,8 @@ class MembersEditorTest extends TestCase $person, $position); - $this->assertEquals(2, $person->getHouseholdParticipations()->count()); + $persistables = $editor->getPersistable(); + $this->assertEquals(1, count($persistables)); $membership2 = $person->getHouseholdParticipations()->last(); $this->assertNull($membership2->getEndDate()); diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index c3067be55..0fac8e6ce 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -192,6 +192,25 @@ components: text: type: string readOnly: true + Household: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - 'household' + HouseholdPosition: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - 'household_position' + paths: /1.0/person/person/{id}.json: @@ -764,3 +783,46 @@ paths: description: "OK" 400: description: "transition cannot be applyed" + + /1.0/person/household/members/move.json: + post: + tags: + - household + summary: move one or multiple person from a household to another + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + concerned: + type: array + items: + type: object + properties: + person: + $ref: '#/components/schemas/PersonById' + start_date: + $ref: '#/components/schemas/Date' + position: + $ref: '#/components/schemas/HouseholdPosition' + holder: + type: boolean + comment: + type: string + destination: + oneOf: + - $ref: '#/components/schemas/Household' + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" + 422: + description: "Unprocessable entity (validation errors)" + 400: + description: "transition cannot be applyed" + diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210528092625.php b/src/Bundle/ChillPersonBundle/migrations/Version20210528092625.php index 5d1909424..1ce855140 100644 --- a/src/Bundle/ChillPersonBundle/migrations/Version20210528092625.php +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210528092625.php @@ -42,7 +42,8 @@ final class Version20210528092625 extends AbstractMigration -- extension btree_gist required to include comparaison with integer person_id WITH =, daterange(startdate, enddate) WITH && - ) WHERE (sharedhousehold IS TRUE)"); + ) WHERE (sharedhousehold IS TRUE) + INITIALLY DEFERRED"); // rename constraints $this->addSql('ALTER TABLE public.chill_person_household_to_addresses DROP CONSTRAINT fk_7109483e79ff843');