feat: forced teamer data verification after configured due dates

addresses #869b33c7p
This commit is contained in:
Björn Fromme
2026-03-15 12:42:28 +01:00
parent 20da6a0afc
commit 518bc7339f
19 changed files with 357 additions and 14 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ use App\BusProNet\Model\BaseDataResponse;
use App\BusProNet\Model\CrmAttributesResponse;
use App\BusProNet\Model\NotificationResponse;
use App\BusProNet\Model\ProfileResponse;
use App\BusProNet\Model\ProfileUpdateResponse;
use App\Entity\User;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
@@ -60,7 +61,7 @@ class ApiClient
* @throws ApiClientException
* @throws ResponseParserException
*/
public function updateProfile(User $user, string $password): NotificationResponse|ProfileResponse
public function updateProfile(User $user, string $password): NotificationResponse|ProfileUpdateResponse
{
if (null === $teamer = $user->getTeamer()) {
throw new ApiClientException('Invalid argument');
@@ -0,0 +1,46 @@
<?php
namespace App\BusProNet\Model;
class ProfileUpdateResponse
{
private ?int $addressId = null;
private ?int $personId = null;
private bool $updated = false;
public function getAddressId(): ?int
{
return $this->addressId;
}
public function setAddressId(?int $addressId): static
{
$this->addressId = $addressId;
return $this;
}
public function getPersonId(): ?int
{
return $this->personId;
}
public function setPersonId(?int $personId): static
{
$this->personId = $personId;
return $this;
}
public function isUpdated(): bool
{
return $this->updated;
}
public function setUpdated(bool $updated): static
{
$this->updated = $updated;
return $this;
}
}
+24 -1
View File
@@ -13,6 +13,7 @@ use App\BusProNet\Model\Hotel;
use App\BusProNet\Model\NotificationResponse;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\ProfileResponse;
use App\BusProNet\Model\ProfileUpdateResponse;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ResponseParser
@@ -45,8 +46,9 @@ class ResponseParser
$subType = (string) $xml->art;
switch ($subType) {
case 'Adressdaten':
case 'Adressdaten_Ändern':
return $this->createProfileResponse($xml);
case 'Adressdaten_Ändern':
return $this->createProfileUpdateResponse($xml);
case 'SelektionCRM':
case 'SelektionCRM_Ändern':
return $this->createCrmAttributesResponse($xml);
@@ -138,6 +140,27 @@ class ResponseParser
return $response;
}
public function createProfileUpdateResponse(\SimpleXMLElement $xml): ProfileUpdateResponse
{
$addressId = (int) $xml->idadresse;
$personId = (int) $xml->idperson;
$updated = false;
$updatedXml = $xml->xpath('änderung|aenderung');
if (0 < count($updatedXml)) {
$updated = 'true' === strtolower((string) $updatedXml[0]);
}
$response = new ProfileUpdateResponse();
$response
->setAddressId($addressId)
->setPersonId($personId)
->setUpdated($updated)
;
return $response;
}
public function createCrmAttributesResponse(\SimpleXMLElement $xml): CrmAttributesResponse
{
$groups = [];
@@ -9,6 +9,7 @@ use App\Entity\Upload;
use App\Entity\User;
use App\Form\TeamerProfileType;
use App\Model\UploadSessionDto;
use App\RequiredTeamerCheck\PersonalDataVerificationRequiredCheck;
use App\Service\Upload\UploadHandler;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -27,6 +28,7 @@ class IndexController extends AbstractController
private readonly ApiClient $apiClient,
private readonly UploadHandler $uploadHandler,
private readonly LoggerInterface $logger,
private readonly PersonalDataVerificationRequiredCheck $personalDataVerificationRequiredCheck,
) {
}
@@ -53,8 +55,11 @@ class IndexController extends AbstractController
// Validate teamer data to show missing data right away
$errors = $this->validator->validate($teamer, null, ['profile_preflight']);
$verificationMode = false === $this->personalDataVerificationRequiredCheck->isSatisfied($user);
$form = $this->createForm(TeamerProfileType::class, $teamer, [
'upload_session' => $uploadSession,
'verification_mode' => $verificationMode,
]);
$form->handleRequest($request);
@@ -66,6 +71,10 @@ class IndexController extends AbstractController
} catch (ApiClientException $e) {
}
if (true === $verificationMode) {
$teamer->setDataVerifiedAt(new \DateTimeImmutable());
}
$this->entityManager->flush();
$this->addFlash('success', 'Deine Daten wurden aktualisiert');
@@ -81,6 +90,7 @@ class IndexController extends AbstractController
'form' => $form->createView(),
'teamer' => $teamer,
'errors' => $errors,
'verification_mode' => $verificationMode,
]);
}
+15
View File
@@ -145,6 +145,9 @@ class Teamer implements TimestampableEntityInterface
#[ORM\Column]
private bool $allowOverlappingApplications = false;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $dataVerifiedAt = null;
#[ORM\Column(nullable: true)]
private ?bool $driverLicenseDeclaration = null;
@@ -922,4 +925,16 @@ class Teamer implements TimestampableEntityInterface
return $this;
}
public function getDataVerifiedAt(): ?\DateTimeImmutable
{
return $this->dataVerifiedAt;
}
public function setDataVerifiedAt(?\DateTimeImmutable $dataVerifiedAt): static
{
$this->dataVerifiedAt = $dataVerifiedAt;
return $this;
}
}
@@ -3,7 +3,7 @@
namespace App\EventListener;
use App\Entity\User;
use App\Security\RequiredCheck\RequiredTeamerCheckRegistry;
use App\RequiredTeamerCheck\RequiredTeamerCheckRegistry;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
+17
View File
@@ -7,6 +7,7 @@ use App\Model\UploadSessionDto;
use App\Service\Upload\UploadHandler;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -14,6 +15,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
class TeamerProfileType extends AbstractType
{
@@ -94,6 +96,19 @@ class TeamerProfileType extends AbstractType
],
])
;
if (true === $options['verification_mode']) {
$builder->add('confirmDataVerification', CheckboxType::class, [
'label' => 'Ich bestätige, dass meine Daten korrekt und aktuell sind',
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'Deine Bestätigung ist erforderlich',
'groups' => ['profile'],
]),
],
]);
}
}
public function buildView(FormView $view, FormInterface $form, array $options): void
@@ -114,9 +129,11 @@ class TeamerProfileType extends AbstractType
'profile',
],
'anti_xss' => true,
'verification_mode' => false,
])
->setRequired(['upload_session'])
->setAllowedTypes('upload_session', UploadSessionDto::class)
->setAllowedTypes('verification_mode', 'bool')
;
}
}
@@ -1,10 +1,11 @@
<?php
namespace App\Security\RequiredCheck;
namespace App\RequiredTeamerCheck;
use App\Entity\Teamer;
use App\Entity\Upload;
use App\Entity\User;
use App\RequiredTeamerCheck\RequiredTeamerCheckInterface;
class DriverLicenseRequiredCheck implements RequiredTeamerCheckInterface
{
@@ -0,0 +1,77 @@
<?php
namespace App\RequiredTeamerCheck;
use App\Entity\User;
use Carbon\CarbonImmutable;
class PersonalDataVerificationRequiredCheck implements RequiredTeamerCheckInterface
{
public function __construct(
private readonly array $dataVerificationDeadlines = ['04-01', '10-01'],
) {
}
public function getCode(): string
{
return 'personal_data_verification';
}
public function appliesTo(User $user): bool
{
return true === $user->hasRole('ROLE_TEAMER');
}
public function isSatisfied(User $user): bool
{
$teamer = $user->getTeamer();
if (null === $teamer) {
return false;
}
return false === $this->isVerificationRequired($teamer->getDataVerifiedAt());
}
public function getRouteName(): string
{
return 'app_teamer_profile_index';
}
private function isVerificationRequired(?\DateTimeImmutable $dataVerifiedAt): bool
{
$deadline = $this->getCurrentDeadline(CarbonImmutable::now()->startOfDay());
if (null === $deadline) {
return false;
}
if (null === $dataVerifiedAt) {
return true;
}
return CarbonImmutable::instance($dataVerifiedAt) < $deadline;
}
private function getCurrentDeadline(CarbonImmutable $now): ?CarbonImmutable
{
$deadlines = array_filter(array_map('trim', $this->dataVerificationDeadlines));
rsort($deadlines);
foreach ($deadlines as $date) {
$deadline = CarbonImmutable::createFromFormat('Y-m-d', sprintf('%s-%s', $now->format('Y'), $date));
if (false === $deadline instanceof CarbonImmutable) {
continue;
}
$deadline = $deadline->startOfDay();
if ($deadline <= $now) {
return $deadline;
}
}
return null;
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Security\RequiredCheck;
namespace App\RequiredTeamerCheck;
use App\Entity\User;
@@ -1,8 +1,9 @@
<?php
namespace App\Security\RequiredCheck;
namespace App\RequiredTeamerCheck;
use App\Entity\User;
use App\RequiredTeamerCheck\RequiredTeamerCheckInterface;
class RequiredTeamerCheckRegistry
{
+1 -1
View File
@@ -8,7 +8,7 @@ use App\BusProNet\Model\CrmAttributesResponse;
use App\BusProNet\Model\ProfileResponse;
use App\BusProNet\UserDataHandler;
use App\Entity\User;
use App\Security\RequiredCheck\RequiredTeamerCheckRegistry;
use App\RequiredTeamerCheck\RequiredTeamerCheckRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;