fix: name missing fields the user cannot edit on personal data page
This commit is contained in:
1 parent
6d2db1b258
commit
0ee8fafbe1
8 files changed
+187
-12
No files matched your search
@@ -20,8 +20,8 @@ class Communication
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
public ?string $mobile = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
#[Assert\Email(message: 'Bitte eine gültige Adresse angeben', mode: 'strict', groups: ['personal_data'])]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_identity'])]
|
||||
#[Assert\Email(message: 'Bitte eine gültige Adresse angeben', mode: 'strict', groups: ['personal_identity'])]
|
||||
public ?string $email = null;
|
||||
|
||||
public bool $newsletter = false;
|
||||
|
||||
@@ -23,10 +23,12 @@ class PersonalData
|
||||
public bool $mutable = false;
|
||||
public ?string $status = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_data'])]
|
||||
// personal_identity holds what the user cannot edit in MyE&P (name and the login e-mail), so the
|
||||
// personal-data form can validate everything else while the completeness check still sees it.
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_identity'])]
|
||||
public ?string $name = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_data'])]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_identity'])]
|
||||
public ?string $firstName = '';
|
||||
|
||||
public ?string $salutation = null;
|
||||
@@ -53,7 +55,7 @@ class PersonalData
|
||||
#[Assert\Valid(groups: ['personal_data'])]
|
||||
public Address $address;
|
||||
|
||||
#[Assert\Valid(groups: ['personal_data'])]
|
||||
#[Assert\Valid(groups: ['personal_data', 'personal_identity'])]
|
||||
public Communication $communication;
|
||||
|
||||
/** @var list<string> */
|
||||
|
||||
@@ -158,9 +158,16 @@ class PersonalDataController extends AbstractController
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
|
||||
// The form leaves these out of validation because the user has no field for them, so they
|
||||
// are reported separately. Skipped for a failed load, which would list every field.
|
||||
$invalidIdentityFields = null !== $personalData->addressId
|
||||
? $this->completenessChecker->invalidIdentityFields($personalData)
|
||||
: [];
|
||||
|
||||
return $this->render('account/personal_data.html.twig', [
|
||||
'personalData' => $personalData,
|
||||
'personalDataForm' => $personalDataForm,
|
||||
'invalidIdentityFields' => $invalidIdentityFields,
|
||||
'newsletterSubscribed' => $newsletterSubscribed,
|
||||
'newsletterPendingConfirmation' => $newsletterPendingConfirmation,
|
||||
]);
|
||||
|
||||
@@ -35,7 +35,7 @@ class RegistrationType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => RegistrationDto::class,
|
||||
'validation_groups' => ['Default', 'personal_data'],
|
||||
'validation_groups' => ['Default', 'personal_data', 'personal_identity'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,22 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
* Validates profile completeness for booking requirements.
|
||||
*
|
||||
* Checks whether a user's personal data contains all required fields needed to create
|
||||
* or edit bookings using Symfony's validation system with the 'personal_data' group.
|
||||
* or edit bookings using Symfony's validation system with the 'personal_data' and
|
||||
* 'personal_identity' groups.
|
||||
* Required fields include name, gender, date of birth, nationality, contact information,
|
||||
* and full address data.
|
||||
*/
|
||||
class ProfileCompletenessChecker
|
||||
{
|
||||
private const VALIDATION_GROUP = 'personal_data';
|
||||
private const VALIDATION_GROUPS = ['personal_data', 'personal_identity'];
|
||||
private const IDENTITY_GROUP = 'personal_identity';
|
||||
|
||||
// Fields in personal_identity, which the user cannot edit in MyE&P, by violation property path.
|
||||
private const IDENTITY_FIELD_LABELS = [
|
||||
'firstName' => 'Vorname',
|
||||
'name' => 'Nachname',
|
||||
'communication.email' => 'E-Mail-Adresse',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ValidatorInterface $validator,
|
||||
@@ -27,7 +36,7 @@ class ProfileCompletenessChecker
|
||||
/**
|
||||
* Checks if the personal data contains all required fields for booking operations.
|
||||
*
|
||||
* Validates against the 'personal_data' group which includes:
|
||||
* Validates against the 'personal_data' and 'personal_identity' groups, which include:
|
||||
* - firstName and name (personal identification)
|
||||
* - gender and dateOfBirth (personal attributes)
|
||||
* - nationality (booking form requirement)
|
||||
@@ -40,8 +49,28 @@ class ProfileCompletenessChecker
|
||||
*/
|
||||
public function isComplete(PersonalData $personalData): bool
|
||||
{
|
||||
$violations = $this->validator->validate($personalData, null, [self::VALIDATION_GROUP]);
|
||||
$violations = $this->validator->validate($personalData, null, self::VALIDATION_GROUPS);
|
||||
|
||||
return 0 === $violations->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the missing or invalid fields the user cannot fix themselves, because only customer
|
||||
* service may change them in BusPro.
|
||||
*
|
||||
* @return list<string> Field labels, in the order of IDENTITY_FIELD_LABELS
|
||||
*/
|
||||
public function invalidIdentityFields(PersonalData $personalData): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach ($this->validator->validate($personalData, null, [self::IDENTITY_GROUP]) as $violation) {
|
||||
$paths[$violation->getPropertyPath()] = true;
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
self::IDENTITY_FIELD_LABELS,
|
||||
static fn (string $path): bool => isset($paths[$path]),
|
||||
\ARRAY_FILTER_USE_KEY,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,16 @@
|
||||
Meine<br>Daten
|
||||
</h1>
|
||||
</div>
|
||||
{% if app.session.get('_profile_completion_redirect') %}
|
||||
{% if invalidIdentityFields is not empty %}
|
||||
<div class="mx-4 lg:mx-8">
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'warning',
|
||||
messages: [
|
||||
'In deinem Kundenkonto fehlen Angaben oder sind fehlerhaft, die du hier nicht selbst ändern kannst: <strong>' ~ (invalidIdentityFields|join(', ')|e) ~ '</strong>. Bitte wende dich an den Kundenservice. Bis dahin ist eine Buchung leider nicht möglich.'
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
{% elseif app.session.get('_profile_completion_redirect') %}
|
||||
<div class="mx-4 lg:mx-8">
|
||||
{% include '_partials/_alert.html.twig' with {
|
||||
level: 'info',
|
||||
|
||||
@@ -56,6 +56,51 @@ class PersonalDataControllerTest extends TestCase
|
||||
self::assertSame(false, $controller->renderedParameters['newsletterPendingConfirmation']);
|
||||
}
|
||||
|
||||
public function testIndexListsIdentityFieldsTheUserCannotFix(): void
|
||||
{
|
||||
$personalData = $this->createPersonalData('', 'Musterfirma GmbH');
|
||||
$completenessChecker = $this->createMock(ProfileCompletenessChecker::class);
|
||||
$completenessChecker
|
||||
->expects(self::once())
|
||||
->method('invalidIdentityFields')
|
||||
->with($personalData)
|
||||
->willReturn(['Vorname']);
|
||||
|
||||
$controller = $this->createController(
|
||||
accountClient: $this->createMock(AccountClient::class),
|
||||
completenessChecker: $completenessChecker,
|
||||
);
|
||||
$controller->accountClient
|
||||
->expects(self::once())
|
||||
->method('getPersonalData')
|
||||
->willReturn($personalData);
|
||||
|
||||
$controller->index(Request::create('/personal-data', 'GET'));
|
||||
|
||||
self::assertSame(['Vorname'], $controller->renderedParameters['invalidIdentityFields']);
|
||||
}
|
||||
|
||||
public function testIndexListsNoIdentityFieldsWhenPersonalDataFailedToLoad(): void
|
||||
{
|
||||
$completenessChecker = $this->createMock(ProfileCompletenessChecker::class);
|
||||
$completenessChecker
|
||||
->expects(self::never())
|
||||
->method('invalidIdentityFields');
|
||||
|
||||
$controller = $this->createController(
|
||||
accountClient: $this->createMock(AccountClient::class),
|
||||
completenessChecker: $completenessChecker,
|
||||
);
|
||||
$controller->accountClient
|
||||
->expects(self::once())
|
||||
->method('getPersonalData')
|
||||
->willReturn(new Notification(500, 'no such customer'));
|
||||
|
||||
$controller->index(Request::create('/personal-data', 'GET'));
|
||||
|
||||
self::assertSame([], $controller->renderedParameters['invalidIdentityFields']);
|
||||
}
|
||||
|
||||
public function testIndexStillTracksPendingNewsletterConfirmationWhenSubscribed(): void
|
||||
{
|
||||
$controller = $this->createController(
|
||||
@@ -217,6 +262,7 @@ class PersonalDataControllerTest extends TestCase
|
||||
?AccountClient $accountClient = null,
|
||||
?NewsletterManager $newsletterManager = null,
|
||||
?FormInterface $form = null,
|
||||
?ProfileCompletenessChecker $completenessChecker = null,
|
||||
): TestablePersonalDataController {
|
||||
$user = new User('[email protected]');
|
||||
$user->setPassword('secret');
|
||||
@@ -239,7 +285,7 @@ class PersonalDataControllerTest extends TestCase
|
||||
$accountClient ?? $this->createStub(AccountClient::class),
|
||||
$crypt,
|
||||
$this->createStub(BookingEditDataLoader::class),
|
||||
$this->createStub(ProfileCompletenessChecker::class),
|
||||
$completenessChecker ?? $this->createStub(ProfileCompletenessChecker::class),
|
||||
$this->createStub(EntityManagerInterface::class),
|
||||
$newsletterManager ?? $this->createStub(NewsletterManager::class),
|
||||
$this->createStub(LoggerInterface::class),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Service\ProfileCompletenessChecker;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class ProfileCompletenessCheckerTest extends TestCase
|
||||
{
|
||||
public function testCompleteProfileHasNoInvalidIdentityFields(): void
|
||||
{
|
||||
$checker = new ProfileCompletenessChecker($this->createValidator());
|
||||
$personalData = $this->createCompletePersonalData();
|
||||
|
||||
self::assertTrue($checker->isComplete($personalData));
|
||||
self::assertSame([], $checker->invalidIdentityFields($personalData));
|
||||
}
|
||||
|
||||
/**
|
||||
* A company name in the last name and no first name is what BusPro hands us for some accounts.
|
||||
*/
|
||||
public function testMissingFirstNameIsReportedAndKeepsTheProfileIncomplete(): void
|
||||
{
|
||||
$checker = new ProfileCompletenessChecker($this->createValidator());
|
||||
$personalData = $this->createCompletePersonalData();
|
||||
$personalData->firstName = null;
|
||||
$personalData->name = 'Musterfirma GmbH';
|
||||
|
||||
self::assertFalse($checker->isComplete($personalData));
|
||||
self::assertSame(['Vorname'], $checker->invalidIdentityFields($personalData));
|
||||
}
|
||||
|
||||
public function testInvalidIdentityFieldsAreListedInFixedOrder(): void
|
||||
{
|
||||
$checker = new ProfileCompletenessChecker($this->createValidator());
|
||||
$personalData = $this->createCompletePersonalData();
|
||||
$personalData->communication->email = 'not-an-address';
|
||||
$personalData->name = '';
|
||||
$personalData->firstName = '';
|
||||
|
||||
self::assertSame(['Vorname', 'Nachname', 'E-Mail-Adresse'], $checker->invalidIdentityFields($personalData));
|
||||
}
|
||||
|
||||
public function testFieldsTheUserCanEditAreNotReportedAsIdentityFields(): void
|
||||
{
|
||||
$checker = new ProfileCompletenessChecker($this->createValidator());
|
||||
$personalData = $this->createCompletePersonalData();
|
||||
$personalData->address->street = null;
|
||||
$personalData->communication->mobile = null;
|
||||
|
||||
self::assertFalse($checker->isComplete($personalData));
|
||||
self::assertSame([], $checker->invalidIdentityFields($personalData));
|
||||
}
|
||||
|
||||
private function createValidator(): ValidatorInterface
|
||||
{
|
||||
return Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
|
||||
}
|
||||
|
||||
private function createCompletePersonalData(): PersonalData
|
||||
{
|
||||
$personalData = new PersonalData();
|
||||
$personalData->firstName = 'Mia';
|
||||
$personalData->name = 'Muster';
|
||||
$personalData->gender = 'W';
|
||||
$personalData->nationality = 'D';
|
||||
$personalData->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
$personalData->address->street = 'Musterweg 1';
|
||||
$personalData->address->postCode = '12345';
|
||||
$personalData->address->city = 'Musterstadt';
|
||||
$personalData->address->country = 'D';
|
||||
$personalData->communication->email = '[email protected]';
|
||||
$personalData->communication->mobile = '0170 1234567';
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user