From f5e66df3787952c7bfff057830f4ab8a2c1f4f62 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 23 Sep 2024 16:02:14 +0200 Subject: [PATCH] 1015: Added check for digital post --- .env | 4 ++ CHANGELOG.md | 4 +- composer.json | 1 + composer.lock | 16 ++++---- config/services.yaml | 1 + src/Command/CacheClearCommand.php | 35 +++++++++++++++++ src/Command/DigitalPostForespoergCommand.php | 5 +++ src/Controller/HearingController.php | 11 ++++++ src/Entity/Party.php | 17 +++++++++ src/Service/PartyHelper.php | 40 +++++++++++++++++++- src/Service/SF1601/DigitalPoster.php | 25 ++++++++++-- 11 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 src/Command/CacheClearCommand.php diff --git a/.env b/.env index b53bf14c..c1b0f17a 100644 --- a/.env +++ b/.env @@ -174,3 +174,7 @@ USER_SIGNATURE_HEIGHT='2cm' SF1601_SENDER_LABEL='Aarhus Kommune' #SF1601_FORSENDELSES_TYPE_IDENTIFIKATOR='' SF1601_TEST_MODE='true' + +# Set to `-1 day`, say, to effectovely disable cache. +# Run `bin/console tvist1:cache:clear` after changing this. +SF1601_POST_FORESPOERG_CACHE_EXPIRE_AT='+1 day' diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd1d952..4d3ccd10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ about writing changes to this log. ## [Unreleased] -* [PR-401](https://github.com/itk-dev/naevnssekretariatet/pull/401) +- [PR-402](https://github.com/itk-dev/naevnssekretariatet/pull/402) + Optimized handling of digital post +- [PR-401](https://github.com/itk-dev/naevnssekretariatet/pull/401) Update itk-dev/serviceplatformen - [PR-399](https://github.com/itk-dev/naevnssekretariatet/pull/399) Added `forsendelse-uuid` filtering option to diff --git a/composer.json b/composer.json index 6af79517..ffe1b641 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "sensio/framework-extra-bundle": "^5.1", "stof/doctrine-extensions-bundle": "^1.7", "symfony/asset": "5.4.*", + "symfony/cache": "5.4.*", "symfony/console": "5.4.*", "symfony/doctrine-messenger": "5.4.*", "symfony/dotenv": "5.4.*", diff --git a/composer.lock b/composer.lock index 5e86ef68..4cf20157 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c990f37b67bc91657a98ee7a4de23783", + "content-hash": "3b6b5be55e35381d58072426e58972bd", "packages": [ { "name": "behat/transliterator", @@ -4990,16 +4990,16 @@ }, { "name": "symfony/cache", - "version": "v5.4.38", + "version": "v5.4.44", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "223c3afac82e003a76931b71d77db408636a0de8" + "reference": "4b3e7bf157b8b5a010865701d9106b5f0a9c99a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/223c3afac82e003a76931b71d77db408636a0de8", - "reference": "223c3afac82e003a76931b71d77db408636a0de8", + "url": "https://api.github.com/repos/symfony/cache/zipball/4b3e7bf157b8b5a010865701d9106b5f0a9c99a8", + "reference": "4b3e7bf157b8b5a010865701d9106b5f0a9c99a8", "shasum": "" }, "require": { @@ -5028,7 +5028,7 @@ "cache/integration-tests": "dev-master", "doctrine/cache": "^1.6|^2.0", "doctrine/dbal": "^2.13.1|^3|^4", - "predis/predis": "^1.1", + "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0", "symfony/config": "^4.4|^5.0|^6.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", @@ -5067,7 +5067,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.4.38" + "source": "https://github.com/symfony/cache/tree/v5.4.44" }, "funding": [ { @@ -5083,7 +5083,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T09:55:32+00:00" + "time": "2024-09-13T16:57:39+00:00" }, { "name": "symfony/cache-contracts", diff --git a/config/services.yaml b/config/services.yaml index 9f76f79b..dba084a9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -267,6 +267,7 @@ services: sender_label: '%env(SF1601_SENDER_LABEL)%' forsendelses_type_identifikator: '%env(int:SF1601_FORSENDELSES_TYPE_IDENTIFIKATOR)%' test_mode: '%env(bool:SF1601_TEST_MODE)%' + post_forespoerg_cache_expire_at: '%env(SF1601_POST_FORESPOERG_CACHE_EXPIRE_AT)%' App\Retry\DigitalPostRetryStrategy: arguments: diff --git a/src/Command/CacheClearCommand.php b/src/Command/CacheClearCommand.php new file mode 100644 index 00000000..65ec852d --- /dev/null +++ b/src/Command/CacheClearCommand.php @@ -0,0 +1,35 @@ +clear()) { + $io->success(sprintf('%s cache cleared', $cache::class)); + } else { + $io->error(sprintf('Error clearing %s cache', $cache::class)); + } + } + + return Command::SUCCESS; + } +} diff --git a/src/Command/DigitalPostForespoergCommand.php b/src/Command/DigitalPostForespoergCommand.php index a7d666f5..d005515f 100644 --- a/src/Command/DigitalPostForespoergCommand.php +++ b/src/Command/DigitalPostForespoergCommand.php @@ -3,6 +3,7 @@ namespace App\Command; use App\Service\SF1601\DigitalPoster; +use ItkDev\Serviceplatformen\Service\SF1601\SF1601; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -41,6 +42,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $table->setHeaders(['Identifier', 'Result']); $identifiers = array_unique($input->getArgument('identifier')); $type = $input->getOption('type'); + if (!in_array($type, SF1601::FORESPOERG_TYPES)) { + throw new \InvalidArgumentException(sprintf('Invalid type: %s. Must be one of %s', $type, implode(', ', SF1601::FORESPOERG_TYPES))); + } + foreach ($identifiers as $identifier) { $result = $this->digitalPoster->canReceive($type, $identifier); $table->appendRow([$identifier, is_bool($result) ? json_encode($result) : $result]); diff --git a/src/Controller/HearingController.php b/src/Controller/HearingController.php index 2c07cfa2..880b5bbd 100644 --- a/src/Controller/HearingController.php +++ b/src/Controller/HearingController.php @@ -193,6 +193,17 @@ public function hearingPostRequestCreate(CaseEntity $case, DocumentUploader $doc } $availableParties = $partyHelper->getRelevantPartiesForHearingPostByCase($case); + + foreach ($availableParties as $type => &$parties) { + foreach ($parties as $key => $party) { + if (!$party->canReceiveDigitalPost()) { + $newKey = $key.sprintf(' (%s)', new TranslatableMessage('Cannot receive digital post')); + $parties[$newKey] = $party; + unset($parties[$key]); + } + } + } + $mailTemplates = $mailTemplateHelper->getTemplates('hearing'); $hearingPost = new HearingPostRequest(); diff --git a/src/Entity/Party.php b/src/Entity/Party.php index 2107fe98..7dcb565d 100644 --- a/src/Entity/Party.php +++ b/src/Entity/Party.php @@ -70,6 +70,11 @@ class Party implements LoggableEntityInterface */ private $isUnderAddressProtection = false; + /** + * Virtual property set on runtime. + */ + private ?bool $canReceiveDigitalPost = null; + public function __construct() { $this->id = Uuid::v4(); @@ -164,4 +169,16 @@ public function setIsUnderAddressProtection(bool $isUnderAddressProtection): sel return $this; } + + public function canReceiveDigitalPost(): ?bool + { + return $this->canReceiveDigitalPost; + } + + public function setCanReceiveDigitalPost(bool $canReceiveDigitalPost): self + { + $this->canReceiveDigitalPost = $canReceiveDigitalPost; + + return $this; + } } diff --git a/src/Service/PartyHelper.php b/src/Service/PartyHelper.php index 83476e1b..1fccda25 100644 --- a/src/Service/PartyHelper.php +++ b/src/Service/PartyHelper.php @@ -7,13 +7,21 @@ use App\Entity\Party; use App\Repository\CasePartyRelationRepository; use App\Repository\PartyRepository; +use App\Service\SF1601\DigitalPoster; use Doctrine\ORM\EntityManagerInterface; +use ItkDev\Serviceplatformen\Service\SF1601\SF1601; use Symfony\Component\Form\FormInterface; use Symfony\Contracts\Translation\TranslatorInterface; class PartyHelper { - public function __construct(private EntityManagerInterface $entityManager, private PartyRepository $partyRepository, private CasePartyRelationRepository $relationRepository, private TranslatorInterface $translator) + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly PartyRepository $partyRepository, + private readonly CasePartyRelationRepository $relationRepository, + private readonly TranslatorInterface $translator, + private readonly DigitalPoster $digitalPoster, + ) { } @@ -273,6 +281,9 @@ private function getSortingCounterpartyType(CaseEntity $case) return reset($counterpartTypes); } + /** + * @return array + */ public function getRelevantPartiesForHearingPostByCase(CaseEntity $case): array { $partyRelations = $this->relationRepository @@ -307,9 +318,36 @@ public function getRelevantPartiesForHearingPostByCase(CaseEntity $case): array // Sort alphabetically uasort($counterparties, static fn ($a, $b) => $a->getName() <=> $b->getName()); + $this->setCanReceiveDigitalPost($parties); + $this->setCanReceiveDigitalPost($counterparties); + return [$this->translator->trans('Parties', [], 'case') => $parties, $this->translator->trans('Counterparties', [], 'case') => $counterparties]; } + /** + * @param Party[] $parties + */ + private function setCanReceiveDigitalPost(array $parties): array + { + foreach ($parties as $party) { + $canReceiveDigitalPost = true; + try { + $identification = $party->getIdentification(); + if (IdentificationHelper::IDENTIFIER_TYPE_CPR === $identification->getType()) { + $canReceiveDigitalPost = $this->digitalPoster->canReceive( + SF1601::FORESPOERG_TYPE_DIGITAL_POST, + $identification->getIdentifier() + ); + } + $party->setCanReceiveDigitalPost($canReceiveDigitalPost); + } catch (\Throwable $t) { + throw $t; + } + } + + return $parties; + } + public function getTransformedRelevantPartiesByCase(CaseEntity $case): array { $parties = $this->getRelevantPartiesByCase($case); diff --git a/src/Service/SF1601/DigitalPoster.php b/src/Service/SF1601/DigitalPoster.php index d64ec5c8..b36433f7 100644 --- a/src/Service/SF1601/DigitalPoster.php +++ b/src/Service/SF1601/DigitalPoster.php @@ -9,8 +9,10 @@ use ItkDev\Serviceplatformen\Service\SF1601\SF1601; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Uid\Uuid; +use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; class DigitalPoster @@ -27,10 +29,26 @@ public function __construct(private CertificateLocatorHelper $certificateLocator public function canReceive(string $type, string $identifier): ?bool { - $service = $this->getSF1601(); - $transactionId = Serializer::createUuid(); + $cache = new FilesystemAdapter(); + $cacheKey = preg_replace( + '#[{}()/\\\\@:]+#', + '_', + implode('|||', [__METHOD__, $type, $identifier]) + ); + + return $cache->get($cacheKey, function (ItemInterface $item) use ($type, $identifier): ?bool { + try { + $service = $this->getSF1601(); + $transactionId = Serializer::createUuid(); + $result = $service->postForespoerg($transactionId, $type, $identifier); + $item->expiresAt(new \DateTimeImmutable($this->options['post_forespoerg_cache_expire_at'])); + } catch (\Throwable $throwable) { + // Never cache is case of error. + $item->expiresAt(new \DateTimeImmutable('2001-01-01')); + } - return $service->postForespoerg($transactionId, $type, $identifier); + return true === ($result['result'] ?? false); + }); } public function sendDigitalPost(DigitalPost $digitalPost, DigitalPost\Recipient $recipient) @@ -174,6 +192,7 @@ private function resolveOptions(array $options): array ->setAllowedTypes('sender_label', 'string') ; }) + ->setDefault('post_forespoerg_cache_expire_at', '+1 day') ->resolve($options) ; }