feat: MailJet list handling with DOI, webhook receiver and API endpoint
addresses #869cut134
This commit is contained in:
@@ -10,12 +10,12 @@ use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Entity\User;
|
||||
use App\Exception\NewsletterProviderException;
|
||||
use App\Htmx\HxTrait;
|
||||
use App\Form\PersonalDataType;
|
||||
use App\Repository\NewsletterOptInConfirmationRepository;
|
||||
use App\Model\NewsletterSubscriptionRequestResult;
|
||||
use App\Service\NewsletterManager;
|
||||
use App\Security\Crypt;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\MailjetApiClient;
|
||||
use App\Service\NewsletterManager;
|
||||
use App\Service\ProfileCompletenessChecker;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -30,31 +30,33 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
*
|
||||
* Provides functionality for viewing and updating customer profile information
|
||||
* through integration with the BusProNet API system. Handles personal data
|
||||
* management and newsletter subscription preferences for authenticated users.
|
||||
* management for authenticated users.
|
||||
*/
|
||||
class PersonalDataController extends AbstractController
|
||||
{
|
||||
use HxTrait;
|
||||
|
||||
public const SESSION_REDIRECT_KEY = '_profile_completion_redirect';
|
||||
|
||||
/**
|
||||
* @param ApiClient $apiClient BusProNet API client for data operations
|
||||
* @param Crypt $crypt Encryption service for password handling
|
||||
* @param BookingEditDataLoader $dataLoader Data loader for cache invalidation
|
||||
* @param ApiClient $apiClient BusProNet API client for data operations
|
||||
* @param Crypt $crypt Encryption service for password handling
|
||||
* @param BookingEditDataLoader $dataLoader Data loader for cache invalidation
|
||||
* @param ProfileCompletenessChecker $completenessChecker Profile validation service
|
||||
* @param EntityManagerInterface $entityManager Entity manager for persisting user changes
|
||||
* @param LoggerInterface $logger Logger for audit trails and debugging
|
||||
* @param EntityManagerInterface $entityManager Entity manager for persisting user changes
|
||||
* @param NewsletterManager $newsletterManager Newsletter confirmation service
|
||||
* @param LoggerInterface $logger Logger for audit trails and debugging
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly Crypt $crypt,
|
||||
private readonly BookingEditDataLoader $dataLoader,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly Crypt $crypt,
|
||||
private readonly BookingEditDataLoader $dataLoader,
|
||||
private readonly ProfileCompletenessChecker $completenessChecker,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MailjetApiClient $newsletterService,
|
||||
private readonly NewsletterManager $doubleOptInService,
|
||||
private readonly NewsletterOptInConfirmationRepository $newsletterConfirmationRepository,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly NewsletterManager $newsletterManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,25 +80,9 @@ class PersonalDataController extends AbstractController
|
||||
$user = $this->getUser();
|
||||
$email = $user->getEmail();
|
||||
$password = $this->crypt->decrypt($user->getPassword());
|
||||
|
||||
try {
|
||||
$personalData = $this
|
||||
->apiClient
|
||||
->getPersonalData($email, $password);
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
|
||||
$personalData = new PersonalData();
|
||||
}
|
||||
|
||||
if ($personalData instanceof Notification) {
|
||||
$this->logger->error('Unable to fetch personal data', [
|
||||
'code' => $personalData->code,
|
||||
'error' => $personalData->message,
|
||||
]);
|
||||
|
||||
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
|
||||
$personalData = new PersonalData();
|
||||
}
|
||||
$personalData = $this->loadPersonalData($user);
|
||||
$newsletterSubscribed = $this->newsletterManager->hasConfirmedOptIn($email);
|
||||
$newsletterPendingConfirmation = $this->newsletterManager->hasPendingConfirmation($email);
|
||||
|
||||
$personalDataForm = $this->createForm(PersonalDataType::class, $personalData, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
@@ -137,22 +123,6 @@ class PersonalDataController extends AbstractController
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
|
||||
$newsletterSubscribed = false;
|
||||
$newsletterPendingConfirmation = false;
|
||||
try {
|
||||
$newsletterSubscribed = $this->newsletterService->isSubscribed($email);
|
||||
} catch (NewsletterProviderException $e) {
|
||||
$this->logger->warning('Unable to read newsletter subscription status', [
|
||||
'email' => $email,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->addFlash('error', 'Der Newsletter-Status konnte gerade nicht geladen werden.');
|
||||
}
|
||||
|
||||
if (!$newsletterSubscribed) {
|
||||
$newsletterPendingConfirmation = null !== $this->newsletterConfirmationRepository->findPendingByEmail($email);
|
||||
}
|
||||
|
||||
return $this->render('account/personal_data.html.twig', [
|
||||
'personalData' => $personalData,
|
||||
'personalDataForm' => $personalDataForm->createView(),
|
||||
@@ -161,17 +131,6 @@ class PersonalDataController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle newsletter subscription status for the authenticated user.
|
||||
*
|
||||
* Retrieves current personal data, toggles the newsletter subscription flag,
|
||||
* and updates the preference via BusProNet API. Designed for HTMX AJAX
|
||||
* requests to provide immediate feedback without full page reload.
|
||||
*
|
||||
* @return Response Redirect response to personal data page
|
||||
*
|
||||
* @throws ApiClientException When BusProNet API communication fails
|
||||
*/
|
||||
#[Route('/personal-data/newsletter', name: 'app_personal_data_newsletter', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function newsletter(Request $request): Response
|
||||
@@ -179,40 +138,80 @@ class PersonalDataController extends AbstractController
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$email = $user->getEmail();
|
||||
|
||||
$shouldSubscribe = $request->request->getBoolean('subscribed');
|
||||
$personalData = $this->loadPersonalData($user);
|
||||
$hasPendingConfirmation = $this->newsletterManager->hasPendingConfirmation($email);
|
||||
|
||||
try {
|
||||
if ($shouldSubscribe) {
|
||||
if ($this->newsletterService->isSubscribed($email)) {
|
||||
$this->addFlash('info', 'Du bist bereits zum Newsletter angemeldet.');
|
||||
} else {
|
||||
$hasPendingConfirmation = null !== $this->newsletterConfirmationRepository->findPendingByEmail($email);
|
||||
$this->doubleOptInService->requestConfirmation($email);
|
||||
if ($hasPendingConfirmation) {
|
||||
$this->addFlash('success', 'Wir haben dir eine neue Bestätigungs-E-Mail gesendet.');
|
||||
} else {
|
||||
$this->addFlash('success', 'Bitte bestätige deine Newsletter-Anmeldung über den Link in der E-Mail.');
|
||||
}
|
||||
}
|
||||
if (true === $hasPendingConfirmation) {
|
||||
$this->newsletterManager->requestConfirmation($email, $personalData->firstName, $personalData->name);
|
||||
$this->addFlash('success', 'Wir haben dir eine neue Bestätigungs-E-Mail gesendet.');
|
||||
} else {
|
||||
$this->newsletterService->unsubscribe($email);
|
||||
$this->addFlash('success', 'Du wurdest vom Newsletter abgemeldet.');
|
||||
$result = $this->newsletterManager->requestDefaultListSubscription($email, $personalData->firstName, $personalData->name);
|
||||
$this->addFlash(...$this->newsletterRequestFlash($result));
|
||||
}
|
||||
|
||||
$this->logger->info('Updated newsletter registration intent', [
|
||||
$this->logger->info('Requested newsletter confirmation', [
|
||||
'email' => $user->getEmail(),
|
||||
'subscribed' => $shouldSubscribe,
|
||||
'pending_confirmation' => $hasPendingConfirmation,
|
||||
]);
|
||||
} catch (NewsletterProviderException|\InvalidArgumentException $e) {
|
||||
$this->addFlash('error', 'Die Newsletter-Aktion konnte gerade nicht verarbeitet werden. Bitte versuche es erneut.');
|
||||
$this->logger->warning('Newsletter action failed', [
|
||||
'email' => $email,
|
||||
'subscribed' => $shouldSubscribe,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
return $this->htmxRedirect($request, $this->generateUrl('app_personal_data'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string}
|
||||
*/
|
||||
private function newsletterRequestFlash(NewsletterSubscriptionRequestResult $result): array
|
||||
{
|
||||
$states = array_unique($result->listStates);
|
||||
|
||||
if (
|
||||
NewsletterSubscriptionRequestResult::STATE_SUBSCRIBED === $result->state
|
||||
&& [NewsletterSubscriptionRequestResult::LIST_STATE_ALREADY_REGISTERED] === array_values($states)
|
||||
) {
|
||||
return ['info', 'Du bist bereits zum Newsletter angemeldet.'];
|
||||
}
|
||||
|
||||
return match ($result->state) {
|
||||
NewsletterSubscriptionRequestResult::STATE_SUBSCRIBED => ['success', 'Deine Newsletter-Anmeldung wurde aktualisiert.'],
|
||||
NewsletterSubscriptionRequestResult::STATE_PENDING_CONFIRMATION => ['info', 'Deine Anmeldung muss noch bestätigt werden. Bitte nutze den Link aus der Bestätigungs-E-Mail.'],
|
||||
default => ['success', 'Bitte bestätige deine Newsletter-Anmeldung über den Link in der E-Mail.'],
|
||||
};
|
||||
}
|
||||
|
||||
private function loadPersonalData(User $user): PersonalData
|
||||
{
|
||||
$email = $user->getEmail();
|
||||
$password = $this->crypt->decrypt($user->getPassword());
|
||||
|
||||
try {
|
||||
$personalData = $this
|
||||
->apiClient
|
||||
->getPersonalData($email, $password);
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
|
||||
|
||||
return new PersonalData();
|
||||
}
|
||||
|
||||
if ($personalData instanceof Notification) {
|
||||
$this->logger->error('Unable to fetch personal data', [
|
||||
'code' => $personalData->code,
|
||||
'error' => $personalData->message,
|
||||
]);
|
||||
|
||||
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
|
||||
|
||||
return new PersonalData();
|
||||
}
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Exception\NewsletterListNotAllowedException;
|
||||
use App\Exception\NewsletterProviderException;
|
||||
use App\Model\NewsletterSubscriptionRequest;
|
||||
use App\Model\NewsletterSubscriptionRequestResult;
|
||||
use App\Service\NewsletterManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class NewsletterSubscriptionController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $mailjetLists
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly NewsletterManager $newsletterManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly array $mailjetLists = [],
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/newsletters', name: 'api_newsletters_all', methods: ['GET'])]
|
||||
public function index():JsonResponse
|
||||
{
|
||||
return $this->json($this->newsletterManager->createListIdMapping(array_keys($this->mailjetLists)));
|
||||
}
|
||||
|
||||
#[Route('/newsletter-subscriptions', name: 'api_newsletters_subscriptions', methods: ['POST'])]
|
||||
public function subscribe(
|
||||
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
|
||||
NewsletterSubscriptionRequest $request,
|
||||
): JsonResponse {
|
||||
try {
|
||||
$result = $this
|
||||
->newsletterManager
|
||||
->requestApiSubscription(
|
||||
(string) $request->email,
|
||||
$request->listIds,
|
||||
array_keys($this->mailjetLists),
|
||||
$request->firstName,
|
||||
$request->lastName,
|
||||
)
|
||||
;
|
||||
} catch (NewsletterListNotAllowedException $exception) {
|
||||
return $this->badRequest($exception->getMessage(), [
|
||||
'listIds' => $exception->getListIds(),
|
||||
'unknownListIds' => $exception->getUnknownListIds(),
|
||||
]);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
return $this->badRequest($exception->getMessage(), [
|
||||
'listIds' => $request->listIds,
|
||||
]);
|
||||
} catch (NewsletterProviderException $exception) {
|
||||
$this->logger->warning('Newsletter subscription request failed', [
|
||||
'email' => $request->email,
|
||||
'list_ids' => $request->listIds,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'email' => $request->email,
|
||||
'listIds' => $request->listIds,
|
||||
'message' => 'Newsletter subscription request could not be processed.',
|
||||
], Response::HTTP_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->responsePayload($result),
|
||||
NewsletterSubscriptionRequestResult::STATE_CONFIRMATION_REQUESTED === $result->state
|
||||
? Response::HTTP_ACCEPTED
|
||||
: Response::HTTP_OK,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extra
|
||||
*/
|
||||
private function badRequest(string $message, array $extra = []): JsonResponse
|
||||
{
|
||||
return new JsonResponse(array_merge([
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
], $extra), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
private function responsePayload(NewsletterSubscriptionRequestResult $result): array
|
||||
{
|
||||
return [
|
||||
'success' => true,
|
||||
'email' => $result->email,
|
||||
'lists' => array_map(
|
||||
fn (array $list): array => array_merge($list, [
|
||||
'state' => $result->stateForList((int) $list['id']),
|
||||
]),
|
||||
$this->newsletterManager->createListIdMapping($result->listIds),
|
||||
),
|
||||
'state' => $result->state,
|
||||
'confirmationRequested' => $result->confirmationRequested,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class PickupController extends AbstractController
|
||||
{
|
||||
private const PLANNING_FILE = 'pickup_planning.json';
|
||||
private const string PLANNING_FILE = 'pickup_planning.json';
|
||||
|
||||
public function __construct(
|
||||
private readonly PickupLoader $xmlLoader,
|
||||
|
||||
@@ -14,11 +14,11 @@ use App\Exception\NewsletterProviderException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\BookingCreateStep4Type;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Htmx\HxTrait;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingCreateContextFactory;
|
||||
use App\Service\BookingSessionManager;
|
||||
use App\Service\MailjetApiClient;
|
||||
use App\Service\NewsletterManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -40,7 +40,6 @@ class Step4Controller extends AbstractBookingCreateController
|
||||
private readonly BookingCreateContextFactory $createContextFactory,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly MailjetApiClient $newsletterService,
|
||||
private readonly NewsletterManager $doubleOptInService,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
@@ -64,17 +63,7 @@ class Step4Controller extends AbstractBookingCreateController
|
||||
}
|
||||
|
||||
$newsletterTargetEmail = $this->resolveNewsletterTargetEmail($bookingCreateDto);
|
||||
$newsletterOptInVisible = false;
|
||||
if (null !== $newsletterTargetEmail) {
|
||||
try {
|
||||
$newsletterOptInVisible = false === $this->newsletterService->isSubscribed($newsletterTargetEmail);
|
||||
} catch (NewsletterProviderException $e) {
|
||||
$this->logger->warning('Could not resolve newsletter subscription state in booking step 4', [
|
||||
'email' => $newsletterTargetEmail,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
$newsletterOptInVisible = null !== $newsletterTargetEmail;
|
||||
|
||||
$form = $this->createForm(BookingCreateStep4Type::class, $bookingCreateDto, [
|
||||
'show_newsletter_opt_in' => $newsletterOptInVisible,
|
||||
@@ -122,7 +111,12 @@ class Step4Controller extends AbstractBookingCreateController
|
||||
|
||||
if (true === $newsletterOptInSelected && null !== $newsletterTargetEmail) {
|
||||
try {
|
||||
$this->doubleOptInService->requestConfirmation($newsletterTargetEmail);
|
||||
$targetParticipant = $this->resolveNewsletterTargetParticipant($bookingCreateDto);
|
||||
$this->doubleOptInService->requestDefaultListSubscription(
|
||||
$newsletterTargetEmail,
|
||||
$targetParticipant?->firstName,
|
||||
$targetParticipant?->lastName,
|
||||
);
|
||||
} catch (NewsletterProviderException|\InvalidArgumentException $e) {
|
||||
$this->logger->warning('Newsletter confirmation request failed after booking', [
|
||||
'email' => $newsletterTargetEmail,
|
||||
@@ -216,6 +210,20 @@ class Step4Controller extends AbstractBookingCreateController
|
||||
return false !== filter_var($normalizedEmail, FILTER_VALIDATE_EMAIL) ? $normalizedEmail : null;
|
||||
}
|
||||
|
||||
private function resolveNewsletterTargetParticipant(BookingDto $bookingDto): ?ParticipantDto
|
||||
{
|
||||
$participant = $bookingDto->participants[0] ?? null;
|
||||
if (null === $participant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null === $participant->firstName && null === $participant->lastName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears travel data and availability cache after successful booking.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Webhook;
|
||||
|
||||
use App\Message\MailjetNewsletterEventMessage;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class MailjetNewsletterWebhookController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/webhooks/mailjet/newsletter', name: 'app_webhook_mailjet_newsletter', methods: ['POST'])]
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (JSON_ERROR_NONE !== json_last_error() || false === is_array($payload)) {
|
||||
return new JsonResponse(['success' => false, 'message' => 'Invalid JSON payload.'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$events = $this->normalizeEvents($payload);
|
||||
$dispatched = 0;
|
||||
|
||||
foreach ($events as $eventPayload) {
|
||||
$message = $this->createMessage($eventPayload);
|
||||
if (null === $message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
++$dispatched;
|
||||
}
|
||||
|
||||
$this->logger->info('Accepted Mailjet newsletter webhook payload', [
|
||||
'events' => count($events),
|
||||
'dispatched' => $dispatched,
|
||||
]);
|
||||
|
||||
return new JsonResponse(['success' => true, 'dispatched' => $dispatched]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $payload
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function normalizeEvents(array $payload): array
|
||||
{
|
||||
if ([] === $payload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (array_is_list($payload)) {
|
||||
return array_values(array_filter($payload, static fn (mixed $entry): bool => is_array($entry)));
|
||||
}
|
||||
|
||||
return [$payload];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function createMessage(array $payload): ?MailjetNewsletterEventMessage
|
||||
{
|
||||
$event = isset($payload['event']) ? strtolower(trim((string) $payload['event'])) : '';
|
||||
if (MailjetNewsletterEventMessage::EVENT_UNSUBSCRIBE !== $event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$email = isset($payload['email']) ? mb_strtolower(trim((string) $payload['email'])) : '';
|
||||
if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mailjetListId = $this->resolveListId($payload);
|
||||
if (null === $mailjetListId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MailjetNewsletterEventMessage(
|
||||
email: $email,
|
||||
mailjetListId: $mailjetListId,
|
||||
event: $event,
|
||||
eventAt: $this->resolveEventAt($payload),
|
||||
payload: $payload,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function resolveListId(array $payload): ?int
|
||||
{
|
||||
$value = $payload['mj_list_id'] ?? $payload['list_id'] ?? null;
|
||||
if (true === is_int($value)) {
|
||||
return $value > 0 ? $value : null;
|
||||
}
|
||||
|
||||
if (true === is_string($value) && true === ctype_digit(trim($value))) {
|
||||
$listId = (int) trim($value);
|
||||
|
||||
return $listId > 0 ? $listId : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function resolveEventAt(array $payload): ?\DateTimeImmutable
|
||||
{
|
||||
$value = $payload['time'] ?? $payload['event_at'] ?? null;
|
||||
if (true === is_int($value) || true === is_float($value) || true === (is_string($value) && ctype_digit(trim($value)))) {
|
||||
return (new \DateTimeImmutable())->setTimestamp((int) $value);
|
||||
}
|
||||
|
||||
if (true === is_string($value) && '' !== trim($value)) {
|
||||
try {
|
||||
return new \DateTimeImmutable($value);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user