feat: offer a newsletter opt-in for the other participants

addresses #869etn6c5
This commit is contained in:
fromme committed 2026-09-26 13:53:10 +02:00
1 parent 96b7b3f451
commit 876e69e62c
10 files changed
+598 -71

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Tests\Form;
use App\BusProNet\Model\Travel;
use App\Form\BookingCreateStep4Type;
use App\Form\Model\BookingDto;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Validator\Validation;
class BookingCreateStep4TypeTest extends TestCase
{
public function testCompanionOptInIsAbsentByDefault(): void
{
$form = $this->createForm();
self::assertFalse($form->has('companionNewsletterOptIn'));
}
public function testCompanionOptInIsAddedWhenTheOptionIsOn(): void
{
$form = $this->createForm(['show_companion_newsletter_opt_in' => true]);
self::assertTrue($form->has('companionNewsletterOptIn'));
self::assertFalse($form->get('companionNewsletterOptIn')->isRequired());
}
public function testCompanionOptInIsIndependentOfTheApplicantOptIn(): void
{
$form = $this->createForm(['show_companion_newsletter_opt_in' => true]);
self::assertFalse($form->has('newsletterOptIn'));
self::assertTrue($form->has('companionNewsletterOptIn'));
}
/**
* @param array<string, mixed> $options
*
* @return FormInterface<mixed>
*/
private function createForm(array $options = []): FormInterface
{
$travel = new Travel();
$travel->id = 123;
return Forms::createFormFactoryBuilder()
->addExtension(new ValidatorExtension(Validation::createValidator()))
->addExtension(new PreloadedExtension([new BookingCreateStep4Type('https://example.com/agb')], []))
->getFormFactory()
->create(BookingCreateStep4Type::class, new BookingDto($travel, 77), $options);
}
}
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Travel;
use App\Enum\NewsletterOptInContext;
use App\Exception\NewsletterProviderException;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\BookingNewsletterOptIn;
use App\Service\NewsletterManager;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
class BookingNewsletterOptInTest extends TestCase
{
public function testChildrenAreNotOfferedACompanionOptIn(): void
{
$bookingDto = $this->createBookingDto([
['[email protected]', 30],
['[email protected]', 12],
]);
$companions = $this->createService($this->createStub(NewsletterManager::class))
->resolveCompanions($bookingDto, '[email protected]');
self::assertSame([], $companions);
}
public function testApplicantAddressIsNotOfferedAsACompanion(): void
{
$bookingDto = $this->createBookingDto([
['[email protected]', 30],
['[email protected] ', 25],
]);
$companions = $this->createService($this->createStub(NewsletterManager::class))
->resolveCompanions($bookingDto, '[email protected]');
self::assertSame([], $companions);
}
public function testDuplicateAndInvalidCompanionAddressesAreCollapsed(): void
{
$bookingDto = $this->createBookingDto([
['[email protected]', 30],
['[email protected]', 25],
['[email protected]', 26],
['not-an-email', 27],
[null, 28],
]);
$companions = $this->createService($this->createStub(NewsletterManager::class))
->resolveCompanions($bookingDto, '[email protected]');
self::assertCount(1, $companions);
self::assertSame('[email protected]', $companions[0]->email);
}
public function testConfirmedAndPendingCompanionsAreSkipped(): void
{
$companions = $this->createCompanions([
'[email protected]',
'[email protected]',
'[email protected]',
]);
$newsletterManager = $this->createMock(NewsletterManager::class);
$newsletterManager
->method('hasConfirmedOptIn')
->willReturnCallback(static fn (string $email): bool => '[email protected]' === $email);
$newsletterManager
->method('hasPendingConfirmation')
->willReturnCallback(static fn (string $email): bool => '[email protected]' === $email);
$newsletterManager
->expects(self::once())
->method('requestConfirmation')
->with('[email protected]', 'Tim', 'Teiler', NewsletterOptInContext::Companion, 'Mia Muster');
$this->createService($newsletterManager)->subscribeCompanions($companions, 'Mia Muster');
}
public function testOneFailingCompanionDoesNotStopTheOthers(): void
{
$companions = $this->createCompanions([
'[email protected]',
'[email protected]',
]);
$newsletterManager = $this->createMock(NewsletterManager::class);
$newsletterManager->method('hasConfirmedOptIn')->willReturn(false);
$newsletterManager->method('hasPendingConfirmation')->willReturn(false);
$requestedEmails = [];
$newsletterManager
->expects(self::exactly(2))
->method('requestConfirmation')
->willReturnCallback(static function (string $email) use (&$requestedEmails): void {
$requestedEmails[] = $email;
if ('[email protected]' === $email) {
throw new NewsletterProviderException('Mailer down.');
}
});
$this->createService($newsletterManager)->subscribeCompanions($companions, 'Mia Muster');
self::assertSame(['[email protected]', '[email protected]'], $requestedEmails);
}
public function testApplicantOptInUsesTheOwnWording(): void
{
$bookingDto = $this->createBookingDto([['[email protected]', 30]]);
$applicant = $bookingDto->participants[0];
$newsletterManager = $this->createMock(NewsletterManager::class);
$newsletterManager
->expects(self::once())
->method('requestConfirmation')
->with('[email protected]', 'Mia', 'Muster');
$this->createService($newsletterManager)->subscribeApplicant('[email protected]', $applicant);
}
public function testRequestedByNameIsTheApplicantsFullName(): void
{
$bookingDto = $this->createBookingDto([['[email protected]', 30]]);
self::assertSame(
'Mia Muster',
$this->createService($this->createStub(NewsletterManager::class))->resolveRequestedByName($bookingDto)
);
}
private function createService(NewsletterManager $newsletterManager): BookingNewsletterOptIn
{
return new BookingNewsletterOptIn($newsletterManager, new NullLogger());
}
/**
* @param list<array{0: string|null, 1: int}> $participants email and age, applicant first
*/
private function createBookingDto(array $participants): BookingDto
{
$bookingDto = new BookingDto(new Travel(), 1);
foreach ($participants as $index => [$email, $age]) {
$participant = new ParticipantDto();
$participant->index = $index;
$participant->firstName = 0 === $index ? 'Mia' : 'Tim';
$participant->lastName = 0 === $index ? 'Muster' : 'Teiler';
$participant->email = $email;
$participant->dateOfBirth = new \DateTimeImmutable(sprintf('-%d years', $age));
$bookingDto->participants[] = $participant;
}
return $bookingDto;
}
/**
* @param list<string> $emails
*
* @return list<ParticipantDto>
*/
private function createCompanions(array $emails): array
{
$companions = [];
foreach ($emails as $offset => $email) {
$participant = new ParticipantDto();
$participant->index = $offset + 1;
$participant->firstName = 'Tim';
$participant->lastName = 'Teiler';
$participant->email = $email;
$participant->dateOfBirth = new \DateTimeImmutable('-25 years');
$companions[] = $participant;
}
return $companions;
}
}
+64 -9
View File
@@ -7,6 +7,7 @@ namespace App\Tests\Service;
use App\Email\Mailer;
use App\Entity\NewsletterConsent;
use App\Entity\NewsletterOptInRequest;
use App\Enum\NewsletterOptInContext;
use App\Exception\NewsletterListNotAllowedException;
use App\Mailjet\ApiClient;
use App\Model\NewsletterConfirmationResult;
@@ -16,7 +17,6 @@ use App\Repository\NewsletterOptInRequestRepository;
use App\Service\NewsletterManager;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
@@ -439,14 +439,69 @@ class NewsletterManagerTest extends TestCase
yield 'boolean' => [true];
}
/**
* @param MockObject&NewsletterOptInRequestRepository $repository
* @param MockObject&NewsletterConsentRepository $consents
* @param MockObject&EntityManagerInterface $entityManager
* @param MockObject&ApiClient $mailjet
* @param MockObject&Mailer $mailer
* @param array<int, string> $mailjetLists
*/
public function testRequestConfirmationUsesOwnWordingByDefault(): void
{
$repository = $this->createStub(NewsletterOptInRequestRepository::class);
$consents = $this->createStub(NewsletterConsentRepository::class);
$entityManager = $this->createStub(EntityManagerInterface::class);
$mailjet = $this->createStub(ApiClient::class);
$mailer = $this->createMock(Mailer::class);
$repository->method('deleteExpiredPendingByEmail')->willReturn(0);
$repository->method('findPendingByEmail')->willReturn(null);
$mailer
->expects(self::once())
->method('createAndSendEmail')
->with(
self::callback(static function (array $context): bool {
self::assertNull($context['requestedByName']);
return true;
}),
self::callback(static function (array $options): bool {
self::assertSame('Deine Anmeldung zum E&P Newsletter', $options['subject']);
self::assertSame('email/newsletter_opt_in.html.twig', $options['template']);
return true;
}),
);
$this->createManager($repository, $consents, $entityManager, $mailjet, $mailer)
->requestConfirmation('[email protected]', 'Mia', 'Muster');
}
public function testRequestConfirmationUsesCompanionWordingAndNamesTheRequester(): void
{
$repository = $this->createStub(NewsletterOptInRequestRepository::class);
$consents = $this->createStub(NewsletterConsentRepository::class);
$entityManager = $this->createStub(EntityManagerInterface::class);
$mailjet = $this->createStub(ApiClient::class);
$mailer = $this->createMock(Mailer::class);
$repository->method('deleteExpiredPendingByEmail')->willReturn(0);
$repository->method('findPendingByEmail')->willReturn(null);
$mailer
->expects(self::once())
->method('createAndSendEmail')
->with(
self::callback(static function (array $context): bool {
self::assertSame('Tim', $context['recipientFirstName']);
self::assertSame('Mia Muster', $context['requestedByName']);
return true;
}),
self::callback(static function (array $options): bool {
self::assertSame('Möchtest du den E&P Newsletter erhalten?', $options['subject']);
self::assertSame('email/newsletter_opt_in_companion.html.twig', $options['template']);
return true;
}),
);
$this->createManager($repository, $consents, $entityManager, $mailjet, $mailer)
->requestConfirmation('[email protected]', 'Tim', 'Teiler', NewsletterOptInContext::Companion, 'Mia Muster');
}
private function createManager(
NewsletterOptInRequestRepository $repository,
NewsletterConsentRepository $consents,