feat: create Mailjet lists for travel mailings
This commit is contained in:
1 parent
d63fa10a56
commit
c16f368293
15 files changed
+851
-1
No files matched your search
@@ -432,6 +432,18 @@ class TravelDataProvider
|
||||
return $this->travelLookupService->mapDateCodeToLabel($dateCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map date code to the travel's start date.
|
||||
*
|
||||
* @param string $dateCode The date code to map
|
||||
*
|
||||
* @return \DateTimeImmutable|null The start date or null if not found
|
||||
*/
|
||||
public function mapDateCodeToStartDate(string $dateCode): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->travelLookupService->mapDateCodeToStartDate($dateCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map hotel code to hotel ID.
|
||||
*
|
||||
|
||||
@@ -127,6 +127,26 @@ class TravelIndex
|
||||
return $this->generateFilesMap()[$dateId]['label'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a date code to the travel's start date.
|
||||
*
|
||||
* @param string $dateCode The date code to resolve
|
||||
*
|
||||
* @return \DateTimeImmutable|null The start date, or null if the code is not found or the travel has none
|
||||
*/
|
||||
public function mapDateCodeToStartDate(string $dateCode): ?\DateTimeImmutable
|
||||
{
|
||||
$dateId = $this->mapDateCodeToId($dateCode);
|
||||
|
||||
if (null === $dateId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$dateFrom = $this->generateFilesMap()[$dateId]['dateFrom'] ?? null;
|
||||
|
||||
return $dateFrom instanceof \DateTimeImmutable ? $dateFrom : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hotel code string to its corresponding hotel ID.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BpnConnect\Model\TravelMailingRecipient;
|
||||
use App\Entity\TravelMailingList;
|
||||
use App\Exception\NewsletterProviderException;
|
||||
use App\Mailjet\ApiClient;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Creates one Mailjet contact list per travel and deletes it again 30 days after the travel
|
||||
* started, together with every contact that is on no other Mailjet list.
|
||||
*/
|
||||
class TravelMailingListManager
|
||||
{
|
||||
private const string DELETE_AFTER = '+30 days';
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $mailjet,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TravelMailingRecipient[] $recipients
|
||||
*
|
||||
* @throws NewsletterProviderException
|
||||
*/
|
||||
public function create(string $travelCode, array $recipients, \DateTimeImmutable $travelStartsAt): TravelMailingList
|
||||
{
|
||||
$listId = $this->mailjet->createContactsList($travelCode);
|
||||
|
||||
// Tracked before the contacts go in, so a list Mailjet already holds is never left without a row
|
||||
// that the cleanup would find.
|
||||
$mailingList = new TravelMailingList($travelCode, $listId, $travelStartsAt->modify(self::DELETE_AFTER));
|
||||
$this->entityManager->persist($mailingList);
|
||||
$this->entityManager->flush();
|
||||
|
||||
try {
|
||||
$jobId = $this->mailjet->addContactsToList($listId, $this->buildContacts($recipients));
|
||||
} catch (NewsletterProviderException $exception) {
|
||||
$this->discardEmptyList($mailingList);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->logger->info('Created Mailjet travel mailing list', [
|
||||
'travel_code' => $travelCode,
|
||||
'mailjet_list_id' => $listId,
|
||||
'mailjet_job_id' => $jobId,
|
||||
'recipients' => count($recipients),
|
||||
]);
|
||||
|
||||
return $mailingList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the orphaned contacts first and the list last, so a run that fails halfway can be
|
||||
* repeated: the list still names whoever is left.
|
||||
*
|
||||
* @return int the number of contacts deleted
|
||||
*
|
||||
* @throws NewsletterProviderException
|
||||
*/
|
||||
public function delete(TravelMailingList $mailingList): int
|
||||
{
|
||||
$listId = $mailingList->getMailjetListId();
|
||||
|
||||
$deletedContacts = 0;
|
||||
foreach ($this->mailjet->getListContactIds($listId) as $contactId) {
|
||||
// Anyone who is, or recently was, on another list is kept: their newsletter consent or
|
||||
// another travel's mailing still needs the contact.
|
||||
if ([$listId] !== array_values(array_unique($this->mailjet->getContactListIds($contactId)))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->mailjet->deleteContact($contactId);
|
||||
++$deletedContacts;
|
||||
}
|
||||
|
||||
$this->mailjet->deleteContactsList($listId);
|
||||
|
||||
$this->entityManager->remove($mailingList);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Deleted Mailjet travel mailing list', [
|
||||
'travel_code' => $mailingList->getTravelCode(),
|
||||
'mailjet_list_id' => $listId,
|
||||
'deleted_contacts' => $deletedContacts,
|
||||
]);
|
||||
|
||||
return $deletedContacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TravelMailingRecipient[] $recipients
|
||||
*
|
||||
* @return list<array{email: string, firstName: ?string}>
|
||||
*/
|
||||
private function buildContacts(array $recipients): array
|
||||
{
|
||||
// Several participants can share one address; the first name that comes with it wins.
|
||||
$contacts = [];
|
||||
foreach ($recipients as $recipient) {
|
||||
$email = mb_strtolower(trim($recipient->email));
|
||||
if ('' === $email || true === isset($contacts[$email])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contacts[$email] = [
|
||||
'email' => $email,
|
||||
'firstName' => '' !== trim($recipient->firstName) ? $recipient->firstName : null,
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($contacts);
|
||||
}
|
||||
|
||||
private function discardEmptyList(TravelMailingList $mailingList): void
|
||||
{
|
||||
try {
|
||||
$this->mailjet->deleteContactsList($mailingList->getMailjetListId());
|
||||
} catch (NewsletterProviderException $exception) {
|
||||
// The row stays, so the scheduled cleanup removes the list later.
|
||||
$this->logger->error('Could not delete Mailjet travel mailing list after a failed import', [
|
||||
'travel_code' => $mailingList->getTravelCode(),
|
||||
'mailjet_list_id' => $mailingList->getMailjetListId(),
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->entityManager->remove($mailingList);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user