Controller action to move members of an household

This commit is contained in:
Julien Fastré 2021-05-31 20:42:07 +02:00
parent ace3b1969e
commit 041b1dfc51
12 changed files with 356 additions and 48 deletions

View File

@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Controller;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception;
use Symfony\Component\Routing\Annotation\Route;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\PersonBundle\Household\MembersEditor;
class HouseholdMemberController extends ApiController
{
/**
* @Route(
* "/api/1.0/person/household/members/move.{_format}",
* name="chill_person_household_members_move"
* )
*/
public function move(Request $request, $_format): Response
{
try {
$editor = $this->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"]]);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,75 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Exception;
use Chill\PersonBundle\Household\MembersEditorFactory;
use Chill\PersonBundle\Household\MembersEditor;
class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
private MembersEditorFactory $factory;
use DenormalizerAwareTrait;
public function __construct(MembersEditorFactory $factory)
{
$this->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;
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Bundle\ChillPersonBundle\Tests\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Doctrine\ORM\EntityManagerInterface;
class HouseholdMemberControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider provideValidData
*/
public function testMoveMember($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',
'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')
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Bundle\ChillPersonBundle\Tests\Entity\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use PHPUnit\Framework\TestCase;
class HouseholdMemberTest extends TestCase
{
public function testPositionSharehousehold()
{
$position = (new Position())
->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());
}
}

View File

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

View File

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

View File

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