feat: create ClickUp task after confirming accommodation booking
This commit is contained in:
@@ -1,353 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Exception\NewsletterProviderException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class MailjetApiClient
|
||||
{
|
||||
private const string DEFAULT_BASE_URL = 'https://api.mailjet.com/v3/REST';
|
||||
private const int NAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* @param array{firstName?: string, lastName?: string} $contactMetadataFields
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ?string $apiKey = null,
|
||||
private readonly ?string $apiSecret = null,
|
||||
private readonly ?string $apiBaseUrl = null,
|
||||
private readonly ?string $defaultListId = null,
|
||||
private readonly array $contactMetadataFields = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function upsertContact(string $email, ?string $firstName = null, ?string $lastName = null): void
|
||||
{
|
||||
$normalizedEmail = $this->normalizeEmail($email);
|
||||
$normalizedFirstName = $this->normalizeName($firstName);
|
||||
$normalizedLastName = $this->normalizeName($lastName);
|
||||
|
||||
if (null === $normalizedFirstName && null === $normalizedLastName) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contactData = $this->buildContactDataPayload($normalizedFirstName, $normalizedLastName);
|
||||
|
||||
if ([] === $contactData) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contactId = $this->findOrCreateContactId($normalizedEmail);
|
||||
|
||||
$this->request('POST', 'contactdata', [
|
||||
'json' => [
|
||||
'ContactID' => $contactId,
|
||||
'Data' => $contactData,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function isSubscribed(string $email, ?int $listId = null): bool
|
||||
{
|
||||
$resolvedListId = $this->resolveListId($listId);
|
||||
|
||||
$normalizedEmail = $this->normalizeEmail($email);
|
||||
$contactId = $this->resolveContactId($normalizedEmail);
|
||||
if (null === $contactId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = $this->request('GET', 'Listrecipient', [
|
||||
'query' => [
|
||||
'Contact' => $contactId,
|
||||
'ContactsList' => $resolvedListId,
|
||||
],
|
||||
]);
|
||||
|
||||
$entries = $response['Data'] ?? [];
|
||||
if (false === is_array($entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (false === is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entryContactId = isset($entry['ContactID']) ? (int) $entry['ContactID'] : null;
|
||||
if ($entryContactId !== $contactId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isActive = true === ($entry['IsActive'] ?? false);
|
||||
$isUnsubscribed = true === ($entry['IsUnsubscribed'] ?? false);
|
||||
|
||||
return $isActive && !$isUnsubscribed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function ensureSubscribed(string $email, ?int $listId = null): void
|
||||
{
|
||||
$resolvedListId = $this->resolveListId($listId);
|
||||
|
||||
$normalizedEmail = $this->normalizeEmail($email);
|
||||
$resource = sprintf('Contactslist/%s/managecontact', $resolvedListId);
|
||||
|
||||
try {
|
||||
$this->request('POST', $resource, [
|
||||
'json' => [
|
||||
'Email' => $normalizedEmail,
|
||||
'Action' => 'addforce',
|
||||
],
|
||||
]);
|
||||
} catch (NewsletterProviderException $exception) {
|
||||
$this->logger->error('Mailjet subscribe failed', [
|
||||
'email' => $normalizedEmail,
|
||||
'list_id' => $resolvedListId,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createEventCallbackUrl(string $eventType, string $url, int $version = 2, bool $isBackup = false): array
|
||||
{
|
||||
return $this->request('POST', 'eventcallbackurl', [
|
||||
'json' => [
|
||||
'EventType' => strtolower(trim($eventType)),
|
||||
'Url' => $url,
|
||||
'Version' => $version,
|
||||
'isBackup' => $isBackup,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteEventCallbackUrl(int $eventCallbackUrlId): void
|
||||
{
|
||||
$this->request('DELETE', sprintf('eventcallbackurl/%d', $eventCallbackUrlId), [
|
||||
'allow_empty' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function findOrCreateContactId(string $email): int
|
||||
{
|
||||
$contactId = $this->resolveContactId($email);
|
||||
if (null !== $contactId) {
|
||||
return $contactId;
|
||||
}
|
||||
|
||||
try {
|
||||
$contactId = $this->createContact($email);
|
||||
if (null !== $contactId) {
|
||||
return $contactId;
|
||||
}
|
||||
} catch (NewsletterProviderException $exception) {
|
||||
$contactId = $this->resolveContactId($email);
|
||||
if (null !== $contactId) {
|
||||
return $contactId;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$contactId = $this->resolveContactId($email);
|
||||
if (null !== $contactId) {
|
||||
return $contactId;
|
||||
}
|
||||
|
||||
throw new NewsletterProviderException(sprintf('Mailjet contact could not be resolved for %s', $email));
|
||||
}
|
||||
|
||||
private function createContact(string $email): ?int
|
||||
{
|
||||
$response = $this->request('POST', 'Contact', [
|
||||
'json' => [
|
||||
'Email' => $email,
|
||||
],
|
||||
]);
|
||||
|
||||
return $this->extractContactId($response);
|
||||
}
|
||||
|
||||
private function resolveContactId(string $email): ?int
|
||||
{
|
||||
$this->assertConfigured();
|
||||
|
||||
$response = $this->request('GET', 'Contact', [
|
||||
'query' => [
|
||||
'Email' => $email,
|
||||
'Limit' => 1,
|
||||
],
|
||||
'allow_404' => true,
|
||||
]);
|
||||
|
||||
return $this->extractContactId($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
*/
|
||||
private function extractContactId(array $response): ?int
|
||||
{
|
||||
$entry = $response['Data'][0] ?? null;
|
||||
if (true === is_array($entry) && true === isset($entry['ID'])) {
|
||||
return (int) $entry['ID'];
|
||||
}
|
||||
|
||||
if (true === isset($response['Data']['ID'])) {
|
||||
return (int) $response['Data']['ID'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{Name: string, Value: string}>
|
||||
*/
|
||||
private function buildContactDataPayload(?string $firstName, ?string $lastName): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
if (null !== $firstName && true === isset($this->contactMetadataFields['firstName'])) {
|
||||
$data[] = [
|
||||
'Name' => $this->contactMetadataFields['firstName'],
|
||||
'Value' => $firstName,
|
||||
];
|
||||
}
|
||||
|
||||
if (null !== $lastName && true === isset($this->contactMetadataFields['lastName'])) {
|
||||
$data[] = [
|
||||
'Name' => $this->contactMetadataFields['lastName'],
|
||||
'Value' => $lastName,
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function request(string $method, string $resource, array $options = []): array
|
||||
{
|
||||
$allow404 = true === ($options['allow_404'] ?? false);
|
||||
$allowEmpty = true === ($options['allow_empty'] ?? false);
|
||||
unset($options['allow_404']);
|
||||
unset($options['allow_empty']);
|
||||
|
||||
$resourcePath = trim($resource, '/');
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request(
|
||||
$method,
|
||||
sprintf('%s/%s', $this->getBaseUrl(), $resourcePath),
|
||||
array_merge($options, [
|
||||
'auth_basic' => sprintf('%s:%s', $this->apiKey, $this->apiSecret),
|
||||
])
|
||||
);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (404 === $statusCode && $allow404) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = $response->getContent(false);
|
||||
if ('' === trim($content)) {
|
||||
if (true === $allowEmpty && $statusCode < 400) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw new NewsletterProviderException(sprintf('Mailjet request returned an empty response for resource %s', $resource));
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $exception) {
|
||||
throw new NewsletterProviderException(sprintf('Mailjet request error for resource %s', $resource), previous: $exception);
|
||||
}
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$payloadSummary = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
$payloadSummary = false === $payloadSummary ? null : $payloadSummary;
|
||||
|
||||
throw new NewsletterProviderException(sprintf('Mailjet request failed with status %d for resource %s%s', $statusCode, $resource, null !== $payloadSummary ? sprintf(' (%s)', $payloadSummary) : ''));
|
||||
}
|
||||
|
||||
return $payload;
|
||||
} catch (\Throwable $exception) {
|
||||
if (true === $allow404 && true === str_contains($exception->getMessage(), '404')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($exception instanceof NewsletterProviderException) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
throw new NewsletterProviderException(sprintf('Mailjet request error for resource %s', $resource), previous: $exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function getBaseUrl(): string
|
||||
{
|
||||
$baseUrl = null !== $this->apiBaseUrl && '' !== trim($this->apiBaseUrl)
|
||||
? trim($this->apiBaseUrl)
|
||||
: self::DEFAULT_BASE_URL;
|
||||
|
||||
return rtrim($baseUrl, '/');
|
||||
}
|
||||
|
||||
private function assertConfigured(): void
|
||||
{
|
||||
if (true === empty($this->apiKey) || true === empty($this->apiSecret)) {
|
||||
throw new NewsletterProviderException('Mailjet newsletter service is not fully configured.');
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveListId(?int $listId): string
|
||||
{
|
||||
$this->assertConfigured();
|
||||
|
||||
$resolvedListId = null !== $listId
|
||||
? (string) $listId
|
||||
: (string) $this->defaultListId;
|
||||
|
||||
if ('' === trim($resolvedListId)) {
|
||||
throw new NewsletterProviderException('Mailjet newsletter list is not configured.');
|
||||
}
|
||||
|
||||
return trim($resolvedListId);
|
||||
}
|
||||
|
||||
private function normalizeEmail(string $email): string
|
||||
{
|
||||
return mb_strtolower(trim($email));
|
||||
}
|
||||
|
||||
private function normalizeName(?string $name): ?string
|
||||
{
|
||||
if (null === $name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmed = trim($name);
|
||||
if ('' === $trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mb_substr($trimmed, 0, self::NAME_MAX_LENGTH);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Entity\NewsletterConsent;
|
||||
use App\Entity\NewsletterOptInRequest;
|
||||
use App\Exception\NewsletterListNotAllowedException;
|
||||
use App\Exception\NewsletterProviderException;
|
||||
use App\Mailjet\ApiClient;
|
||||
use App\Model\NewsletterConfirmationResult;
|
||||
use App\Model\NewsletterSubscriptionRequestResult;
|
||||
use App\Repository\NewsletterConsentRepository;
|
||||
@@ -27,7 +28,7 @@ class NewsletterManager
|
||||
private readonly NewsletterOptInRequestRepository $optInRequestRepository,
|
||||
private readonly NewsletterConsentRepository $consentRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MailjetApiClient $newsletterService,
|
||||
private readonly ApiClient $newsletterService,
|
||||
private readonly Mailer $mailer,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly int $newsletterConfirmationTtlHours,
|
||||
|
||||
Reference in New Issue
Block a user