From 8e0d144dd17e768e06b497e498fee5d30e78d63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 6 Mar 2023 15:41:13 +0100 Subject: [PATCH 01/59] Feature: add columns into Address to track the matching with AddressReference --- src/Bundle/ChillMainBundle/Entity/Address.php | 40 ++++++++++++++++++- .../migrations/Version20230306142148.php | 31 ++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20230306142148.php diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 9a0f8b7b3..29c9eb14e 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -12,6 +12,10 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity; use Chill\MainBundle\Doctrine\Model\Point; +use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; +use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; +use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; +use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\ThirdPartyBundle\Entity\ThirdParty; use DateTime; use DateTimeInterface; @@ -28,8 +32,28 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * @ORM\Table(name="chill_main_address") * @ORM\HasLifecycleCallbacks */ -class Address +class Address implements TrackCreationInterface, TrackUpdateInterface { + use TrackCreationTrait; + use TrackUpdateTrait; + + /** + * When an Address does match with the AddressReference + */ + public const ADDR_REFERENCE_STATUS_MATCH = 'match'; + + /** + * When an Address does not match with the AddressReference, and + * is pending for a review + */ + public const ADDR_REFERENCE_STATUS_TO_REVIEW = 'to_review'; + + /** + * When an Address does not match with the AddressReference, but + * is reviewed + */ + public const ADDR_REFERENCE_STATUS_REVIEWED = 'reviewed'; + /** * @ORM\ManyToOne(targetEntity=AddressReference::class) * @Groups({"write"}) @@ -42,7 +66,7 @@ class Address * @ORM\Column(type="string", length=255, nullable=true) * @Groups({"write"}) */ - private $buildingName; + private string $buildingName = ''; /** * @ORM\Column(type="boolean") @@ -165,6 +189,17 @@ class Address */ private ?PostalCode $postcode = null; + /** + * @var self::ADDR_REFERENCE_STATUS_* + * @ORM\Column(type="text", nullable=false, options={"default": self::ADDR_REFERENCE_STATUS_MATCH}) + */ + private string $refStatus = self::ADDR_REFERENCE_STATUS_MATCH; + + /** + * @ORM\Column(type="datetime_immutable", nullable=false, options={"default": "CURRENT_TIMESTAMP"}) + */ + private \DateTimeImmutable $refStatusLastUpdate; + /** * @var string|null * @@ -210,6 +245,7 @@ class Address public function __construct() { $this->validFrom = new DateTime(); + $this->refStatusLastUpdate = new \DateTimeImmutable('now'); $this->geographicalUnits = new ArrayCollection(); } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230306142148.php b/src/Bundle/ChillMainBundle/migrations/Version20230306142148.php new file mode 100644 index 000000000..760d8c826 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20230306142148.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE chill_main_address ADD refStatus TEXT DEFAULT \'match\' NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD refStatusLastUpdate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL'); + + // we must set the last status update to the address reference date to avoid inconsistencies + $this->addSql('UPDATE chill_main_address a SET refStatusLastUpdate = COALESCE(r.updatedat, r.createdat, \'1970-01-01\'::timestamp) FROM chill_main_address_reference r WHERE a.addressreference_id = r.id'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_address DROP refStatus'); + $this->addSql('ALTER TABLE chill_main_address DROP refStatusLastUpdate'); + } +} From c9fe5a393f33818b34cc8bfb2726a393123d32ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 6 Mar 2023 16:18:15 +0100 Subject: [PATCH 02/59] DX: [Address] fix inconsistencies between doctrine mapping and sql schema DX: [Address] fix inconsistencies between doctrine mapping and sql schema Fixed: Address vue module do now set empty value to empty string instead of null DX: fixed AddressReferenceBaseImporterTest --- src/Bundle/ChillMainBundle/Entity/Address.php | 174 ++++++++---------- .../Entity/AddressReference.php | 46 ++--- .../vuejs/Address/components/AddAddress.vue | 30 +-- .../_components/Entity/AddressRenderBox.vue | 6 +- .../Templating/Entity/AddressRender.php | 37 ++-- .../AddressReferenceBaseImporterTest.php | 2 +- .../migrations/Version20230306145728.php | 109 +++++++++++ .../migrations/Version20230306151218.php | 58 ++++++ 8 files changed, 305 insertions(+), 157 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20230306145728.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20230306151218.php diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 29c9eb14e..23b0f971c 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -61,67 +61,48 @@ class Address implements TrackCreationInterface, TrackUpdateInterface private ?AddressReference $addressReference = null; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ private string $buildingName = ''; /** - * @ORM\Column(type="boolean") + * @ORM\Column(type="boolean", options={"default": false}) * @Groups({"write"}) */ private bool $confidential = false; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $corridor; + private string $corridor = ''; /** - * A list of metadata, added by customizable fields. - * - * @var array - */ - private $customs = []; - - /** - * @var string|null - * * used for the CEDEX information * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $distribution; + private string $distribution = ''; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $extra; + private string $extra = ''; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $flat; + private string $flat = ''; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $floor; + private string $floor = ''; /** * List of geographical units and addresses. @@ -155,11 +136,9 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * True if the address is a "no address", aka homeless person, ... * * @Groups({"write"}) - * @ORM\Column(type="boolean") - * - * @var bool + * @ORM\Column(type="boolean", options={"default": false}) */ - private $isNoAddress = false; + private bool $isNoAddress = false; /** * A ThirdParty reference for person's addresses that are linked to a third party. @@ -170,7 +149,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * @Groups({"write"}) * @ORM\JoinColumn(nullable=true, onDelete="SET NULL") */ - private $linkedToThirdParty; + private ?ThirdParty $linkedToThirdParty; /** * A geospatial field storing the coordinates of the Address. @@ -180,7 +159,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * @ORM\Column(type="point", nullable=true) * @Groups({"write"}) */ - private $point; + private ?Point $point = null; /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") @@ -201,28 +180,25 @@ class Address implements TrackCreationInterface, TrackUpdateInterface private \DateTimeImmutable $refStatusLastUpdate; /** - * @var string|null * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $steps; + private string $steps = ''; /** - * @var string * - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $street = ''; + private string $street = ''; /** - * @var string * - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $streetNumber = ''; + private string $streetNumber = ''; /** * Indicates when the address starts validation. Used to build an history @@ -256,7 +232,6 @@ class Address implements TrackCreationInterface, TrackUpdateInterface ->setBuildingName($original->getBuildingName()) ->setConfidential($original->getConfidential()) ->setCorridor($original->getCorridor()) - ->setCustoms($original->getCustoms()) ->setDistribution($original->getDistribution()) ->setExtra($original->getExtra()) ->setFlat($original->getFlat()) @@ -287,7 +262,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface return $this->addressReference; } - public function getBuildingName(): ?string + public function getBuildingName(): string { return $this->buildingName; } @@ -297,35 +272,27 @@ class Address implements TrackCreationInterface, TrackUpdateInterface return $this->confidential; } - public function getCorridor(): ?string + public function getCorridor(): string { return $this->corridor; } - /** - * Get customs informations in the address. - */ - public function getCustoms(): array - { - return $this->customs; - } - - public function getDistribution(): ?string + public function getDistribution(): string { return $this->distribution; } - public function getExtra(): ?string + public function getExtra(): string { return $this->extra; } - public function getFlat(): ?string + public function getFlat(): string { return $this->flat; } - public function getFloor(): ?string + public function getFloor(): string { return $this->floor; } @@ -376,12 +343,22 @@ class Address implements TrackCreationInterface, TrackUpdateInterface return $this->postcode; } - public function getSteps(): ?string + public function getRefStatus(): string + { + return $this->refStatus; + } + + public function getRefStatusLastUpdate(): \DateTimeImmutable + { + return $this->refStatusLastUpdate; + } + + public function getSteps(): string { return $this->steps; } - public function getStreet(): ?string + public function getStreet(): string { return $this->street; } @@ -390,6 +367,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * Get streetAddress1 (legacy function). * * @return string + * @deprecated */ public function getStreetAddress1() { @@ -400,13 +378,14 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * Get streetAddress2 (legacy function). * * @return string + * @deprecated */ public function getStreetAddress2() { return $this->streetNumber; } - public function getStreetNumber(): ?string + public function getStreetNumber(): string { return $this->streetNumber; } @@ -414,7 +393,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface /** * @return DateTime */ - public function getValidFrom() + public function getValidFrom(): DateTime { return $this->validFrom; } @@ -443,7 +422,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface public function setBuildingName(?string $buildingName): self { - $this->buildingName = $buildingName; + $this->buildingName = (string) $buildingName; return $this; } @@ -457,47 +436,35 @@ class Address implements TrackCreationInterface, TrackUpdateInterface public function setCorridor(?string $corridor): self { - $this->corridor = $corridor; - - return $this; - } - - /** - * Store custom informations in the address. - * - * @return $this - */ - public function setCustoms(array $customs): self - { - $this->customs = $customs; + $this->corridor = (string) $corridor; return $this; } public function setDistribution(?string $distribution): self { - $this->distribution = $distribution; + $this->distribution = (string) $distribution; return $this; } public function setExtra(?string $extra): self { - $this->extra = $extra; + $this->extra = (string) $extra; return $this; } public function setFlat(?string $flat): self { - $this->flat = $flat; + $this->flat = (string) $flat; return $this; } public function setFloor(?string $floor): self { - $this->floor = $floor; + $this->floor = (string) $floor; return $this; } @@ -544,19 +511,40 @@ class Address implements TrackCreationInterface, TrackUpdateInterface return $this; } + /** + * Update the ref status + * + * @param Address::ADDR_REFERENCE_STATUS_* $refStatus + * @param bool|null $updateLastUpdate Also update the "refStatusLastUpdate" + */ + public function setRefStatus(string $refStatus, ?bool $updateLastUpdate = true): self + { + $this->refStatus = $refStatus; + + if ($updateLastUpdate) { + $this->setRefStatusLastUpdate(new \DateTimeImmutable('now')); + } + + return $this; + } + + public function setRefStatusLastUpdate(\DateTimeImmutable $refStatusLastUpdate): self + { + $this->refStatusLastUpdate = $refStatusLastUpdate; + + return $this; + } + public function setSteps(?string $steps): self { - $this->steps = $steps; + $this->steps = (string) $steps; return $this; } public function setStreet(?string $street): self { - if (null === $street) { - $street = ''; - } - $this->street = $street; + $this->street = (string) $street; return $this; } @@ -567,6 +555,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * @param string $streetAddress1 * * @return Address + * @deprecated */ public function setStreetAddress1($streetAddress1) { @@ -579,7 +568,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface * Set streetAddress2 (legacy function). * * @param string $streetAddress2 - * + * @deprecated * @return Address */ public function setStreetAddress2($streetAddress2) @@ -591,10 +580,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface public function setStreetNumber(?string $streetNumber): self { - if (null === $streetNumber) { - $streetNumber = ''; - } - $this->streetNumber = $streetNumber; + $this->streetNumber = (string) $streetNumber; return $this; } @@ -641,7 +627,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface return; } - if (empty($this->getStreetAddress1())) { + if ('' === $this->getStreet()) { $context ->buildViolation('address.street1-should-be-set') ->atPath('streetAddress1') diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index 68cc32186..57421749f 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -55,13 +55,13 @@ class AddressReference * @ORM\Column(type="integer") * @groups({"read"}) */ - private $id; + private ?int $id; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $municipalityCode; + private string $municipalityCode = ''; /** * A geospatial field storing the coordinates of the Address. @@ -71,7 +71,7 @@ class AddressReference * @ORM\Column(type="point") * @groups({"read"}) */ - private $point; + private ?Point $point = null; /** * @var PostalCode @@ -79,31 +79,31 @@ class AddressReference * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") * @groups({"read"}) */ - private $postcode; + private ?PostalCode $postcode; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $refId; + private string $refId = ''; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $source; + private string $source = ''; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $street; + private string $street = ''; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $streetNumber; + private string $streetNumber = ''; /** * @ORM\Column(type="datetime_immutable", nullable=true) @@ -126,7 +126,7 @@ class AddressReference return $this->id; } - public function getMunicipalityCode(): ?string + public function getMunicipalityCode(): string { return $this->municipalityCode; } @@ -141,27 +141,27 @@ class AddressReference * * @return PostalCode */ - public function getPostcode() + public function getPostcode(): ?PostalCode { return $this->postcode; } - public function getRefId(): ?string + public function getRefId(): string { return $this->refId; } - public function getSource(): ?string + public function getSource(): string { return $this->source; } - public function getStreet(): ?string + public function getStreet(): string { return $this->street; } - public function getStreetNumber(): ?string + public function getStreetNumber(): string { return $this->streetNumber; } @@ -192,7 +192,7 @@ class AddressReference public function setMunicipalityCode(?string $municipalityCode): self { - $this->municipalityCode = $municipalityCode; + $this->municipalityCode = (string) $municipalityCode; return $this; } @@ -227,21 +227,21 @@ class AddressReference public function setSource(?string $source): self { - $this->source = $source; + $this->source = (string) $source; return $this; } public function setStreet(?string $street): self { - $this->street = $street; + $this->street = (string) $street; return $this; } public function setStreetNumber(?string $streetNumber): self { - $this->streetNumber = $streetNumber; + $this->streetNumber = (string) $streetNumber; return $this; } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue index 21ab5e3d0..41279771f 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -59,7 +59,7 @@ v-bind:insideModal="false" @pick-address="this.pickAddress" ref="suggestAddress"> - + - + @@ -133,7 +133,7 @@ v-bind:insideModal="false" @getCities="getCities" @getReferenceAddresses="getReferenceAddresses"> - + - + @@ -206,7 +206,7 @@ v-bind:flag="this.flag" v-bind:insideModal="false" ref="dateAddress"> - + - + @@ -580,15 +580,15 @@ export default { this.entity.selected.city = this.context.edit ? this.entity.address.postcode : {}; this.entity.selected.address = {}; - this.entity.selected.address.street = this.context.edit ? this.entity.address.street: null; - this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: null; - this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: null; - this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: null; - this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: null; - this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: null; - this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: null; - this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: null; - this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: null; + this.entity.selected.address.street = this.context.edit ? this.entity.address.street: ''; + this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: ''; + this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: ''; + this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: ''; + this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: ''; + this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: ''; + this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: ''; + this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: ''; + this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: ''; this.entity.selected.writeNew.address = this.context.edit && this.entity.address.addressReference === null && this.entity.address.street.length > 0 this.entity.selected.writeNew.postcode = false // NB: this used to be this.context.edit, but think it was erroneous; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue index 2e88bc1cb..2352e394a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue @@ -13,15 +13,15 @@

-

{{ address.text }}

-

{{ address.postcode.code }} {{ address.postcode.name }}

-

{{ address.country.name.fr }}

diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php index 4db8abe9b..62362956b 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php @@ -60,9 +60,6 @@ class AddressRender implements ChillEntityRenderInterface } /** - * @param Address addr - * @param mixed $addr - * * @return string[] */ public function renderLines(Address $addr, bool $includeCityLine = true, bool $includeCountry = true): array @@ -98,18 +95,18 @@ class AddressRender implements ChillEntityRenderInterface } } - return array_values(array_filter($lines, static fn ($l) => null !== $l)); + return array_values(array_filter($lines, static fn ($l) => '' !== (string) $l)); } public function renderStreetLine(Address $addr): ?string { - if (null !== $addr->getStreet() && $addr->getStreet() !== '') { + if ('' !== $addr->getStreet()) { $street = $addr->getStreet(); } else { $street = ''; } - if (null !== $addr->getStreetNumber() && $addr->getStreetNumber() !== '') { + if ('' !== $addr->getStreetNumber()) { $streetNumber = $addr->getStreetNumber(); } else { $streetNumber = ''; @@ -146,7 +143,7 @@ class AddressRender implements ChillEntityRenderInterface private function renderBuildingLine(Address $addr): ?string { - if (null !== $addr->getBuildingName() && $addr->getBuildingName() !== '') { + if ($addr->getBuildingName() !== '') { $building = $addr->getBuildingName(); } else { $building = ''; @@ -172,7 +169,7 @@ class AddressRender implements ChillEntityRenderInterface return $res; } - private function renderCityLine($addr): string + private function renderCityLine(Address $addr): string { if (null !== $addr->getPostcode()) { $res = strtr('{postcode} {label}', [ @@ -180,11 +177,9 @@ class AddressRender implements ChillEntityRenderInterface '{label}' => $addr->getPostcode()->getName(), ]); - if (null !== $addr->getPostCode()->getCountry()->getCountryCode()) { - if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') { - if ($addr->getDistribution()) { - $res = $res . ' ' . $addr->getDistribution(); - } + if ($addr->getPostcode()->getCountry()->getCountryCode() === 'FR') { + if ('' !== $addr->getDistribution()) { + $res = $res . ' ' . $addr->getDistribution(); } } } @@ -192,35 +187,35 @@ class AddressRender implements ChillEntityRenderInterface return $res ?? ''; } - private function renderCountryLine($addr): ?string + private function renderCountryLine(Address $addr): ?string { return $this->translatableStringHelper->localize( - $addr->getPostCode()->getCountry()->getName() + $addr->getPostcode()->getCountry()->getName() ); } - private function renderDeliveryLine($addr): ?string + private function renderDeliveryLine(Address $addr): string { return $addr->getExtra(); } - private function renderIntraBuildingLine($addr): ?string + private function renderIntraBuildingLine(Address $addr): ?string { $arr = []; - if ($addr->getFlat()) { + if ('' !== $addr->getFlat()) { $arr[] = 'appart ' . $addr->getFlat(); } - if ($addr->getFloor()) { + if ('' !== $addr->getFloor()) { $arr[] = 'ét ' . $addr->getFloor(); } - if ($addr->getCorridor()) { + if ('' !== $addr->getCorridor()) { $arr[] = 'coul ' . $addr->getCorridor(); } - if ($addr->getSteps()) { + if ('' !== $addr->getSteps()) { $arr[] = 'esc ' . $addr->getSteps(); } diff --git a/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressReferenceBaseImporterTest.php b/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressReferenceBaseImporterTest.php index 3c14020d5..2ef7f0143 100644 --- a/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressReferenceBaseImporterTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressReferenceBaseImporterTest.php @@ -99,7 +99,7 @@ final class AddressReferenceBaseImporterTest extends KernelTestCase 'abcc guessed fixed' ); - $this->assertCount('1', $addresses); + $this->assertCount(1, $addresses); $this->assertEquals('Rue test abccc guessed fixed', $addresses[0]->getStreet()); $this->assertEquals($previousAddressId, $addresses[0]->getId()); } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230306145728.php b/src/Bundle/ChillMainBundle/migrations/Version20230306145728.php new file mode 100644 index 000000000..a5e1ffa6f --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20230306145728.php @@ -0,0 +1,109 @@ +addSql('ALTER TABLE chill_main_address ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD createdBy_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ADD updatedBy_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER street TYPE TEXT'); + $this->addSql('UPDATE chill_main_address SET street=\'\' WHERE street IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER street SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_main_address ALTER streetnumber TYPE TEXT'); + $this->addSql('UPDATE chill_main_address SET streetnumber=\'\' WHERE streetnumber IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER streetnumber SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_main_address ALTER floor SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET floor=\'\' WHERE floor IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER floor SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER corridor SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET corridor=\'\' WHERE corridor IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER corridor SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER steps SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET steps=\'\' WHERE steps IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER steps SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER buildingname TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address ALTER buildingname SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET buildingname=\'\' WHERE buildingname IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER buildingname SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER flat SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET flat=\'\' WHERE flat IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER flat SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER distribution TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address ALTER distribution SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET distribution=\'\' WHERE distribution IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER distribution SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER extra TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address ALTER extra SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address SET extra=\'\' WHERE extra IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER extra SET NOT NULL'); + $this->addSql('UPDATE chill_main_address SET confidential=FALSE WHERE confidential IS NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER confidential SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER refstatuslastupdate TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('COMMENT ON COLUMN chill_main_address.point IS \'(DC2Type:point)\''); + $this->addSql('COMMENT ON COLUMN chill_main_address.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_address.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_address.refStatusLastUpdate IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F63174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F665FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_165051F63174800F ON chill_main_address (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_165051F665FF1AEC ON chill_main_address (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_address_reference.point IS \'(DC2Type:point)\''); + + } + + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException('down method is not double-checked'); + + $this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT FK_165051F63174800F'); + $this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT FK_165051F665FF1AEC'); + $this->addSql('DROP INDEX IDX_165051F63174800F'); + $this->addSql('DROP INDEX IDX_165051F665FF1AEC'); + $this->addSql('ALTER TABLE chill_main_address ADD customs JSONB DEFAULT \'[]\''); + $this->addSql('ALTER TABLE chill_main_address DROP createdAt'); + $this->addSql('ALTER TABLE chill_main_address DROP updatedAt'); + $this->addSql('ALTER TABLE chill_main_address DROP createdBy_id'); + $this->addSql('ALTER TABLE chill_main_address DROP updatedBy_id'); + $this->addSql('ALTER TABLE chill_main_address ALTER buildingName TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address ALTER buildingName DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER buildingName DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER confidential SET DEFAULT false'); + $this->addSql('ALTER TABLE chill_main_address ALTER confidential DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER corridor DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER corridor DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER distribution TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address ALTER distribution DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER distribution DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER extra TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address ALTER extra DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER extra DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER flat DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER flat DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER floor DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER floor DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER isNoAddress SET DEFAULT false'); + $this->addSql('ALTER TABLE chill_main_address ALTER point TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address ALTER refStatusLastUpdate TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE chill_main_address ALTER steps DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER steps DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address ALTER street TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address ALTER street DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address ALTER streetNumber TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address ALTER streetNumber DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN chill_main_address.refstatuslastupdate IS NULL'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230306151218.php b/src/Bundle/ChillMainBundle/migrations/Version20230306151218.php new file mode 100644 index 000000000..d3d467bea --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20230306151218.php @@ -0,0 +1,58 @@ +addSql('ALTER TABLE chill_main_address_reference ALTER refid TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER refid SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER street TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER street SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address_reference SET street = \'\' WHERE street IS NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER street SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER streetnumber TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER streetnumber SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address_reference SET streetnumber = \'\' WHERE streetnumber IS NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER streetnumber SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER municipalitycode TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER municipalitycode SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address_reference SET municipalitycode = \'\' WHERE municipalitycode IS NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER municipalitycode SET NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER source TYPE TEXT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER source SET DEFAULT \'\''); + $this->addSql('UPDATE chill_main_address_reference SET source = \'\' WHERE source IS NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER source SET NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException('not double-checked'); + + $this->addSql('ALTER TABLE chill_main_address_reference ALTER municipalityCode TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER municipalityCode DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER municipalityCode DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER refId TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER refId DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER source TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER source DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER source DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER street TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER street DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER street DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER streetNumber TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER streetNumber DROP DEFAULT'); + $this->addSql('ALTER TABLE chill_main_address_reference ALTER streetNumber DROP NOT NULL'); + } +} From c56ae08faefaa02e5488dbc32d7fac40779a6baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 7 Mar 2023 22:16:16 +0100 Subject: [PATCH 03/59] Feature: [address reference] write logic to match existing addresses with updates Feature: [address reference] match addresses with reference after import fix sql query for address reference matcher --- .../AddressReferenceBEFromBestAddress.php | 16 ++- .../Import/AddressReferenceFromBano.php | 7 +- .../Import/AddressToReferenceMatcher.php | 87 ++++++++++++ .../Import/AddressToReferenceMatcherTest.php | 127 ++++++++++++++++++ 4 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Service/Import/AddressToReferenceMatcher.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Services/Import/AddressToReferenceMatcherTest.php diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php index 8dfc84bc3..88a9cb4c5 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php @@ -24,12 +24,18 @@ class AddressReferenceBEFromBestAddress private AddressReferenceBaseImporter $baseImporter; + private AddressToReferenceMatcher $addressToReferenceMatcher; + private HttpClientInterface $client; - public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter) - { + public function __construct( + HttpClientInterface $client, + AddressReferenceBaseImporter $baseImporter, + AddressToReferenceMatcher $addressToReferenceMatcher + ) { $this->client = $client; $this->baseImporter = $baseImporter; + $this->addressToReferenceMatcher = $addressToReferenceMatcher; } public function import(string $lang, array $lists): void @@ -89,16 +95,18 @@ class AddressReferenceBEFromBestAddress $record['municipality_objectid'], $record['postal_info_objectid'], $record['streetname'], - $record['housenumber'] . $record['boxnumber'], + $record['housenumber'] .($record['boxnumber'] !== '' ? ' bte '. $record['boxnumber'] : ''), 'bestaddress.' . $list, - (float) $record['X'], (float) $record['Y'], + (float) $record['X'], 3812 ); } $this->baseImporter->finalize(); + $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); + gzclose($uncompressedStream); } } diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBano.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBano.php index 7a9659884..d02a846ec 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBano.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceFromBano.php @@ -22,12 +22,15 @@ class AddressReferenceFromBano { private AddressReferenceBaseImporter $baseImporter; + private AddressToReferenceMatcher $addressToReferenceMatcher; + private HttpClientInterface $client; - public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter) + public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter, AddressToReferenceMatcher $addressToReferenceMatcher) { $this->client = $client; $this->baseImporter = $baseImporter; + $this->addressToReferenceMatcher = $addressToReferenceMatcher; } public function import(string $departementNo): void @@ -82,6 +85,8 @@ class AddressReferenceFromBano $this->baseImporter->finalize(); + $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); + fclose($file); } } diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressToReferenceMatcher.php b/src/Bundle/ChillMainBundle/Service/Import/AddressToReferenceMatcher.php new file mode 100644 index 000000000..de37cfc15 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressToReferenceMatcher.php @@ -0,0 +1,87 @@ += NOW()) + -- only addresses that are marked matching or "to review", but before the update + AND + (a.refstatus LIKE '{{ matching }}' + OR (a.refstatus LIKE '{{ reviewed }}' AND a.refstatuslastupdate < ar.updatedat)) + AND ( + a.postcode_id != ar.postcode_id + OR a.street != ar.street + OR a.streetnumber != ar.streetnumber + OR ROUND(ST_X(a.point) * 1000000) <> ROUND(ST_X(ar.point) * 1000000) + OR ROUND(ST_Y(a.point) * 1000000) <> ROUND(ST_Y(ar.point) * 1000000) + ) + SQL; + + private const SQL_MARK_MATCHING_ADDRESSES_REVIEWED_OR_TO_REVIEW = <<= NOW()) + AND a.refstatus IN ('{{ to_review }}', '{{ reviewed }}') + AND a.postcode_id = ar.postcode_id + AND a.street = ar.street + AND a.streetnumber = ar.streetnumber + AND ROUND(ST_X(a.point) * 1000000) = ROUND(ST_X(ar.point) * 1000000) + AND ROUND(ST_Y(a.point) * 1000000) = ROUND(ST_Y(ar.point) * 1000000) + SQL; + + private const SUBSTITUTES = [ + '{{ to_review }}' => Address::ADDR_REFERENCE_STATUS_TO_REVIEW, + '{{ matching }}' => Address::ADDR_REFERENCE_STATUS_MATCH, + '{{ reviewed }}' => Address::ADDR_REFERENCE_STATUS_REVIEWED + ]; + + public function __construct(Connection $connection, LoggerInterface $logger) + { + $this->connection = $connection; + $this->logger = $logger; + } + + public function checkAddressesMatchingReferences(): void + { + $this->logger->notice(self::LOG_PREFIX.'Starting addresses matching'); + + $this->connection->transactional(function () { + $markedAsMatching = $this->connection->executeStatement( + strtr(self::SQL_MARK_MATCHING_ADDRESSES_REVIEWED_OR_TO_REVIEW, self::SUBSTITUTES) + ); + + $markedAsToReview = $this->connection->executeStatement( + strtr(self::SQL_MARK_TO_REVIEW_ADDRESS_UNMATCHING, self::SUBSTITUTES) + ); + + $this->logger->info(self::LOG_PREFIX.'Executed address matching', [ + 'marked_as_matching' => $markedAsMatching, + 'marked_as_to_review' => $markedAsToReview, + ]); + }); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressToReferenceMatcherTest.php b/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressToReferenceMatcherTest.php new file mode 100644 index 000000000..ad2324012 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Services/Import/AddressToReferenceMatcherTest.php @@ -0,0 +1,127 @@ +addressToReferenceMatcher = self::$container->get(AddressToReferenceMatcher::class); + $this->addressReferenceRepository = self::$container->get(AddressReferenceRepository::class); + $this->countryRepository = self::$container->get(CountryRepository::class); + $this->addressReferenceBaseImporter = self::$container->get(AddressReferenceBaseImporter::class); + $this->entityManager = self::$container->get(EntityManagerInterface::class); + } + + public function testCheckAddressesMatchingReferences(): void + { + if (null === $belgium = $this->countryRepository->findOneBy(['countryCode' => 'BE'])) { + throw new \RuntimeException('Belgium not found'); + } + + $postalCode = (new PostalCode()) + ->setName('test for matcher') + ->setPostalCodeSource('test for matcher') + ->setCode('78910') + ->setRefPostalCodeId($refPostalCodeId = '78910'.uniqid()) + ->setCountry($belgium) + ; + $this->entityManager->persist($postalCode); + $this->entityManager->flush(); + + $this->addressReferenceBaseImporter->importAddress( + $refAddress = '010203_test'.uniqid(), + $refPostalCodeId, + '78910', + $street = 'Rue Poulet', + $streetNumber = '14', + 'test_matcher', + $lat = 50.0123456789, + $lon = 5.0123456789, + 4326 + ); + $this->addressReferenceBaseImporter->finalize(); + + if (null === $addressReference = $this->addressReferenceRepository->findOneBy(['refId' => $refAddress])) { + throw new \RuntimeException('address reference not created'); + } + + $address = Address::createFromAddressReference($addressReference); + + $this->assertEquals('Rue Poulet', $address->getStreet()); + + $this->entityManager->persist($address); + $this->entityManager->flush(); + + // we update the address + $this->addressReferenceBaseImporter->importAddress( + $refAddress, + $refPostalCodeId, + '78910', + 'Rue Poulet aux amandes', + '14', + 'test_matcher', + 50.01234456789, + 5.0123456789, + 4326 + ); + $this->addressReferenceBaseImporter->finalize(); + + $this->entityManager->flush(); + + $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); + + $this->entityManager->refresh($address); + + $this->assertEquals('to_review', $address->getRefStatus()); + + // we update the address + $this->addressReferenceBaseImporter->importAddress( + $refAddress, + $refPostalCodeId, + '78910', + $street, + $streetNumber, + 'test_matcher', + $lat, + $lon, + 4326 + ); + $this->addressReferenceBaseImporter->finalize(); + + $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); + + $this->entityManager->refresh($address); + + $this->assertEquals('Rue Poulet', $address->getStreet()); + $this->assertEquals('match', $address->getRefStatus()); + } +} From b740a88ae3f00c380a380aa40f1a01c75b4bce7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 14 Mar 2023 22:12:30 +0100 Subject: [PATCH 04/59] Feature: bootstrapping an app to show a modal for address details and showing it inside twig address's render box Feature: showing map and link to external map, and address details inside address details modal Feature: AddressDetailsMap show a warning if the address is incomplete --- package.json | 7 +- .../Resources/public/lib/api/address.ts | 14 +++ .../public/module/address-details/index.ts | 25 +++++ .../ChillMainBundle/Resources/public/types.ts | 9 ++ .../Resources/public/vuejs/Address/api.js | 10 +- .../AddressDetails/AddressDetailsButton.vue | 45 +++++++++ .../AddressDetails/AddressDetailsContent.vue | 22 +++++ .../AddressDetails/AddressModal.vue | 49 ++++++++++ .../Parts/AddressDetailsMap.vue | 91 +++++++++++++++++++ .../Resources/views/Entity/address.html.twig | 4 + .../Resources/views/layout.html.twig | 2 + .../Normalizer/AddressNormalizer.php | 4 + .../Templating/Entity/AddressRender.php | 6 +- .../Templating/Entity/AddressRenderTest.php | 1 + .../ChillMainBundle/chill.webpack.config.js | 1 + src/Bundle/import-png.d.ts | 4 + 16 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue create mode 100644 src/Bundle/import-png.d.ts diff --git a/package.json b/package.json index 17b02c63a..bb4889983 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "chill", - "version": "2.0.0", - "devDependencies": { + "name": "chill", + "version": "2.0.0", + "devDependencies": { "@alexlafroscia/yaml-merge": "^4.0.0", "@apidevtools/swagger-cli": "^4.0.4", "@babel/core": "^7.20.5", @@ -41,6 +41,7 @@ "@fullcalendar/timegrid": "^5.11.0", "@fullcalendar/vue3": "^5.11.1", "@popperjs/core": "^2.9.2", + "@types/leaflet": "^1.9.3", "dropzone": "^5.7.6", "es6-promise": "^4.2.8", "leaflet": "^1.7.1", diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts new file mode 100644 index 000000000..d039daf12 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts @@ -0,0 +1,14 @@ +import {Address} from "../../types"; + +export const getAddressById = async (address_id: number): Promise
=> +{ + const url = `/api/1.0/main/address/${address_id}.json`; + + const response = await fetch(url); + + if (response.ok) { + return response.json(); + } + + throw Error('Error with request resource response'); +}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts b/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts new file mode 100644 index 000000000..aa843bca8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts @@ -0,0 +1,25 @@ +import AddressDetailsButton from "../../vuejs/_components/AddressDetails/AddressDetailsButton.vue"; +import {createApp} from "vue"; +import {createI18n} from "vue-i18n"; +import {_createI18n} from "../../vuejs/_js/i18n"; + +const i18n = _createI18n({}); + +document.querySelectorAll('span[data-address-details]').forEach((el) => { + const dataset = el.dataset as { + addressId: string + }; + + const app = createApp({ + components: {AddressDetailsButton}, + data() { + return { + addressId: Number.parseInt(dataset.addressId), + } + }, + template: '' + }); + + app.use(i18n); + app.mount(el); +}); diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 9453c89b0..3deb1d229 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -70,6 +70,8 @@ export interface Country { code: string; } +export type AddressRefStatus = 'match'|'to_review'|'reviewed'; + export interface Address { type: "address"; address_id: number; @@ -90,6 +92,13 @@ export interface Address { addressReference: AddressReference | null; validFrom: DateTime; validTo: DateTime | null; + point: Point | null; + refStatus: AddressRefStatus; + isNoAddress: boolean; +} + +export interface AddressWithPoint extends Address { + point: Point } export interface AddressReference { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js index b1489bfb6..294ea9480 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js @@ -1,3 +1,5 @@ +import {getAddressById} from 'ChillMainAssets/lib/api/address'; + /** * Endpoint chill_api_single_country__index * method GET, get Country Object @@ -188,13 +190,7 @@ const postPostalCode = (postalCode) => { //<-- * @returns {Promise} a promise containing a Address object */ const getAddress = (id) => { - //console.log('<< get address'); - const url = `/api/1.0/main/address/${id}.json`; - return fetch(url) - .then(response => { - if (response.ok) { return response.json(); } - throw Error('Error with request resource response'); - }); + return getAddressById(id); }; export { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue new file mode 100644 index 000000000..8b1f9d869 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue new file mode 100644 index 000000000..95378b543 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue new file mode 100644 index 000000000..154273a27 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue new file mode 100644 index 000000000..967b16d8c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig index eef84b8a2..9ad2b0a12 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig @@ -69,6 +69,7 @@ {% endif %} {{ _self.inline(address, options, streetLine, lines) }} + {%- endif -%} @@ -78,6 +79,7 @@ {% endif %} {{ _self.inline(address, options, streetLine, lines) }} + {%- endif -%} @@ -102,6 +104,7 @@
{{ 'address.consider homeless'|trans }}
+

{% else %}
@@ -109,6 +112,7 @@ {% endif %} {{ _self.raw(lines) }} +

{% endif %} {{ _self.validity(address, options) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig index 92454be35..8a64c9543 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig @@ -20,6 +20,7 @@ {{ encore_entry_link_tags('chill') }} {{ encore_entry_link_tags('mod_blur') }} {{ encore_entry_link_tags('vue_onthefly') }} + {{ encore_entry_link_tags('mod_address_details') }} {% block css %}{% endblock %} @@ -112,6 +113,7 @@ {{ encore_entry_script_tags('mod_blur') }} {{ encore_entry_script_tags('chill') }} {{ encore_entry_script_tags('vue_onthefly') }} + {{ encore_entry_script_tags('mod_address_details') }} + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue index 967b16d8c..35a2bbc33 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue @@ -3,8 +3,7 @@ Cette adresse est incomplète. La position géographique est approximative.
- Google Maps - OSM +

Voir sur Google Maps OSM

- diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue index 5a9a97b46..d56ad303d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue @@ -1,5 +1,6 @@ @@ -9,6 +10,7 @@ import {Address} from "../../../types"; import AddressDetailsMap from "./Parts/AddressDetailsMap.vue"; import AddressRenderBox from "../Entity/AddressRenderBox.vue"; import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue"; +import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue"; interface AddressModalContentProps { address: Address, @@ -16,6 +18,15 @@ interface AddressModalContentProps { const props = defineProps(); +const emit = defineEmits<{ + (e: 'update-address', value: Address): void +}>(); + +const onUpdateAddress = (address: Address): void => { + console.log('from details content', address); + emit('update-address', address); +} + diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig index 9ad2b0a12..7592e9a9a 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/address.html.twig @@ -69,7 +69,7 @@ {% endif %} {{ _self.inline(address, options, streetLine, lines) }} - + {%- endif -%} @@ -79,7 +79,7 @@ {% endif %} {{ _self.inline(address, options, streetLine, lines) }} - + {%- endif -%} @@ -104,7 +104,7 @@
{{ 'address.consider homeless'|trans }}
-

+

{% else %}
@@ -112,7 +112,7 @@ {% endif %} {{ _self.raw(lines) }} -

+

{% endif %} {{ _self.validity(address, options) }} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/AddressToReferenceMatcherControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/AddressToReferenceMatcherControllerTest.php new file mode 100644 index 000000000..5c51910dc --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/AddressToReferenceMatcherControllerTest.php @@ -0,0 +1,114 @@ +addressRepository = self::$container->get(AddressRepository::class); + } + + /** + * @dataProvider addressToReviewProvider + */ + public function testMarkAddressAsReviewed(int $addressId): void + { + $client = $this->getClientAuthenticated(); + + $client->request('POST', "/api/1.0/main/address/reference-match/${addressId}/set/reviewed"); + + $this->assertResponseIsSuccessful(); + + $address = $this->addressRepository->find($addressId); + + $this->assertEquals(Address::ADDR_REFERENCE_STATUS_REVIEWED, $address->getRefStatus()); + } + + /** + * @dataProvider addressUnsyncedProvider + */ + public function testSyncAddressWithReference(int $addressId): void + { + $client = $this->getClientAuthenticated(); + + $client->request('POST', "/api/1.0/main/address/reference-match/${addressId}/sync-with-reference"); + + $this->assertResponseIsSuccessful(); + + $address = $this->addressRepository->find($addressId); + + $this->assertEquals(Address::ADDR_REFERENCE_STATUS_MATCH, $address->getRefStatus()); + $this->assertEquals($address->getAddressReference()->getStreet(), $address->getStreet()); + $this->assertEquals($address->getAddressReference()->getStreetNumber(), $address->getStreetNumber()); + $this->assertEquals($address->getAddressReference()->getPoint()->toWKT(), $address->getPoint()->toWKT()); + } + + public static function addressToReviewProvider(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $nb = $em->createQuery('SELECT count(a) FROM '.Address::class.' a') + ->getSingleScalarResult(); + + if (0 === $nb) { + throw new \RuntimeException("There aren't any address with a ref status 'matched'"); + } + + /** @var Address $address */ + $address = $em->createQuery('SELECT a FROM '.Address::class.' a') + ->setFirstResult(rand(0, $nb)) + ->setMaxResults(1) + ->getSingleResult(); + + $address->setRefStatus(Address::ADDR_REFERENCE_STATUS_TO_REVIEW); + $em->flush(); + + yield [$address->getId()]; + } + + public static function addressUnsyncedProvider(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $nb = $em->createQuery('SELECT count(a) FROM '.AddressReference::class.' a') + ->getSingleScalarResult(); + + if (0 === $nb) { + throw new \RuntimeException("There isn't any address reference"); + } + + $ref = $em->createQuery('SELECT a FROM '.AddressReference::class.' a') + ->setMaxResults(1) + ->setFirstResult(rand(0, $nb)) + ->getSingleResult(); + + $address = Address::createFromAddressReference($ref); + + // make the address dirty + $address->setStreet('tagada') + ->setStreetNumber('-250') + ->setPoint(Point::fromLonLat(0, 0)) + ->setRefStatus(Address::ADDR_REFERENCE_STATUS_TO_REVIEW); + + $em->persist($address); + $em->flush(); + + yield [$address->getId()]; + } +} From f256dda6fe69c642fff13f5af689d686dcda46e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 20 Mar 2023 18:27:18 +0100 Subject: [PATCH 08/59] Feature: [address] apply details button on address-render-box --- .../AddressToReferenceMatcherController.php | 27 +++++++++++++++++++ .../Resources/public/lib/api/address.ts | 4 +++ .../public/module/address-details/index.ts | 4 +++ .../AddressDetails/AddressDetailsButton.vue | 1 - .../AddressDetails/AddressDetailsContent.vue | 3 +-- .../AddressDetails/AddressModal.vue | 1 - .../Parts/AddressDetailsMap.vue | 3 ++- .../Parts/AddressDetailsRefMatching.vue | 18 +++++++++---- .../_components/Entity/AddressRenderBox.vue | 12 +++++++-- .../_components/Entity/PersonRenderBox.vue | 1 + 10 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php b/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php index a601df13d..7e0debe4d 100644 --- a/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php +++ b/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php @@ -51,6 +51,33 @@ class AddressToReferenceMatcherController ); } + /** + * Set an address back to "to review". Only if the address is in "reviewed" state. + * + * @Route("/api/1.0/main/address/reference-match/{id}/set/to_review", methods={"POST"}) + */ + public function markAddressAsToReview(Address $address): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + if (Address::ADDR_REFERENCE_STATUS_REVIEWED !== $address->getRefStatus()) { + throw new AccessDeniedHttpException("forbidden to mark a matching address to 'to review'"); + } + + $address->setRefStatus(Address::ADDR_REFERENCE_STATUS_TO_REVIEW); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } + /** * @Route("/api/1.0/main/address/reference-match/{id}/sync-with-reference", methods={"POST"}) */ diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts index 66e90a194..1499f4a6a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts @@ -29,3 +29,7 @@ export const syncAddressWithReference = async (address: Address): Promise => { return makeFetch("POST", `/api/1.0/main/address/reference-match/${address.address_id}/set/reviewed`); } + +export const markAddressToReview = async (address: Address): Promise
=> { + return makeFetch("POST", `/api/1.0/main/address/reference-match/${address.address_id}/set/to_review`); +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts b/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts index 4f74c6205..9bc2409ee 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts @@ -23,6 +23,10 @@ document.querySelectorAll('span[data-address-details]').forEach template: '', methods: { onUpdateAddress: (address: Address): void => { + if (address.refStatus === 'to_review' || address.refStatus === 'reviewed') { + // in this two case, the address content do not change + return; + } if (window.confirm("L'adresse a été modifiée. Vous pouvez continuer votre travail. Cependant, pour afficher les données immédiatement, veuillez recharger la page. \n\n Voulez-vous recharger la page immédiatement ?")) { window.location.reload(); } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue index b34b1a374..7e060de4c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue @@ -53,7 +53,6 @@ async function clickOrOpen(): Promise { } const onUpdateAddress = (address: Address): void => { - console.log('from details button', address); data.working_address = address; data.working_ref_status = address.refStatus; emit('update-address', address); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue index d56ad303d..3f6495b0a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue @@ -1,5 +1,5 @@