diff --git a/Api/BrevoClient.php b/Api/BrevoClient.php index 165ba54..f33f748 100644 --- a/Api/BrevoClient.php +++ b/Api/BrevoClient.php @@ -27,14 +27,14 @@ use Brevo\Client\Model\RemoveContactFromList; use Brevo\Client\Model\UpdateContact; use Brevo\Model\BrevoNewsletterQuery; +use Brevo\Trait\DataExtractorTrait; use GuzzleHttp\Client; -use Propel\Runtime\Connection\ConnectionWrapper; -use Propel\Runtime\Propel; use Thelia\Core\Event\Newsletter\NewsletterEvent; use Thelia\Exception\TheliaProcessException; -use Thelia\Log\Tlog; use Thelia\Model\ConfigQuery; use Thelia\Model\Customer; +use Thelia\Model\CustomerQuery; +use Thelia\Model\NewsletterQuery; /** * Class BrevoClient. @@ -43,6 +43,8 @@ */ class BrevoClient { + use DataExtractorTrait; + protected ContactsApi $contactApi; private mixed $newsletterId; @@ -65,7 +67,8 @@ public function subscribe(NewsletterEvent $event) if ($apiException->getCode() !== 404) { throw $apiException; } - $contact = $this->createContact($event->getId()); + + return $this->createContact($event->getEmail()); } $this->update($event, $contact); @@ -78,16 +81,21 @@ public function checkIfContactExist($email) return $this->contactApi->getContactInfoWithHttpInfo($email); } - public function createContact(Customer $customer) + public function createContact(string $email) { - $contactAttribute = $this->getCustomerAttribute($customer->getId()); + $contactAttribute = []; + + if (null !== $customer = CustomerQuery::create()->findOneByEmail($email)) { + $contactAttribute = $this->getCustomerAttribute($customer->getId()); + } + $createContact = new CreateContact(); - $createContact['email'] = $customer->getEmail(); + $createContact['email'] = $email; $createContact['attributes'] = $contactAttribute; $createContact['listIds'] = [$this->newsletterId]; $this->contactApi->createContactWithHttpInfo($createContact); - return $this->contactApi->getContactInfoWithHttpInfo($customer->getEmail()); + return $this->contactApi->getContactInfoWithHttpInfo($email); } public function updateContact($identifier, Customer $customer) @@ -110,13 +118,16 @@ public function update(NewsletterEvent $event, $contact = null) if (!$contact) { $sibObject = BrevoNewsletterQuery::create()->findPk($event->getId()); if (null === $sibObject) { - $sibObject = BrevoNewsletterQuery::create()->findOneByEmail($previousEmail); + $sibObject = NewsletterQuery::create()->findPk($event->getId()); } - $previousEmail = $sibObject->getEmail(); - $contact = $this->contactApi->getContactInfoWithHttpInfo($previousEmail); - $updateContact['email'] = $event->getEmail(); - $updateContact['attributes'] = ['PRENOM' => $event->getFirstname(), 'NOM' => $event->getLastname()]; + if (null !== $sibObject) { + $previousEmail = $sibObject->getEmail(); + $contact = $this->contactApi->getContactInfoWithHttpInfo($previousEmail); + + $updateContact['email'] = $event->getEmail(); + $updateContact['attributes'] = ['PRENOM' => $event->getFirstname(), 'NOM' => $event->getLastname()]; + } } $updateContact['listIds'] = [$this->newsletterId]; @@ -125,80 +136,42 @@ public function update(NewsletterEvent $event, $contact = null) return $this->contactApi->getContactInfoWithHttpInfo($previousEmail); } - public function unsubscribe(NewsletterEvent $event) + public function unsubscribe(string $email) { - $contact = $this->contactApi->getContactInfoWithHttpInfo($event->getEmail()); + $contact = $this->contactApi->getContactInfoWithHttpInfo($email); $change = false; if (\in_array($this->newsletterId, $contact[0]['listIds'], true)) { $contactIdentifier = new RemoveContactFromList(); - $contactIdentifier['emails'] = [$event->getEmail()]; + $contactIdentifier['emails'] = [$email]; $this->contactApi->removeContactFromList($this->newsletterId, $contactIdentifier); $change = true; } - return $change ? $this->contactApi->getContactInfoWithHttpInfo($event->getEmail()) : $contact; + return $change ? $this->contactApi->getContactInfoWithHttpInfo($email) : $contact; } - public function getCustomerAttribute($customerId) + /** + * @throws \JsonException + */ + public function getCustomerAttribute($customerId): array { - try { - if (null === $mapping = json_decode(ConfigQuery::read(Brevo::BREVO_ATTRIBUTES_MAPPING), true, 512, \JSON_THROW_ON_ERROR)) { - throw new TheliaProcessException("Customer attribute mapping error: JSON data seems invalid, pleas echeck syntax."); - } - - if (empty($mapping)) { - return []; - } + $mappingString = ConfigQuery::read(Brevo::BREVO_ATTRIBUTES_MAPPING); - if (!\array_key_exists('customer_query', $mapping)) { - throw new TheliaProcessException("Customer attribute mapping error : 'customer_query' element is missing in JSON data"); - } - - $attributes = []; - - /** @var ConnectionWrapper $con */ - $con = Propel::getConnection(); - - foreach ($mapping['customer_query'] as $key => $customerDataQuery) { - if (!\array_key_exists('select', $customerDataQuery)) { - throw new \Exception("Customer attribute mapping error : 'select' element missing in ".$key.' query'); - } - - try { - $sql = 'SELECT '.$customerDataQuery['select'].' AS '.$key.' FROM customer'; - - if (\array_key_exists('join', $customerDataQuery)) { - foreach ($customerDataQuery['join'] as $join) { - $sql .= ' LEFT JOIN '.$join; - } - } - - $sql .= ' WHERE customer.id = :customerId'; - - if (\array_key_exists('groupBy', $customerDataQuery)) { - $sql .= ' GROUP BY '.$customerDataQuery['groupBy']; - } - - $stmt = $con->prepare($sql); - $stmt->bindValue(':customerId', $customerId, \PDO::PARAM_INT); - $stmt->execute(); - - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - $attributes[$key] = $row[$key]; - if (\array_key_exists($key, $mapping) && \array_key_exists($row[$key], $mapping[$key])) { - $attributes[$key] = $mapping[$key][$row[$key]]; - } - } - } catch (\Exception $ex) { - Tlog::getInstance()->error( - 'Failed to execute SQL request to map Brevo attribute. Error is '.$ex->getMessage().", request is : $sql"); - } - } + if (empty($mappingString)) { + return []; + } - return $attributes; - } catch (\Exception $ex) { - throw new TheliaProcessException('Customer attribute mapping error : configuration is missing or invalid, please go to the module configuration and define the JSON mapping to match thelia attribute with brevo attribute'); + if (null === $mapping = json_decode($mappingString, true)) { + throw new TheliaProcessException('Customer attribute mapping error: JSON data seems invalid, pleas echeck syntax.'); } + + return $this->getMappedValues( + $mapping, + 'customer_query', + 'customer', + 'customer.id', + $customerId, + ); } } diff --git a/Brevo.php b/Brevo.php index ea056c7..c915ec0 100644 --- a/Brevo.php +++ b/Brevo.php @@ -34,6 +34,7 @@ class Brevo extends BaseModule const CONFIG_AUTOMATION_KEY= "brevo.automation.key"; const CONFIG_THROW_EXCEPTION_ON_ERROR = "brevo.throw_exception_on_error"; const BREVO_ATTRIBUTES_MAPPING = "brevo.brevo_attributes_mapping"; + const BREVO_METADATA_MAPPING = "brevo.brevo_metadata_mapping"; public function postActivation(ConnectionInterface $con = null): void { diff --git a/Config/module.xml b/Config/module.xml index f807240..d2e6f3a 100644 --- a/Config/module.xml +++ b/Config/module.xml @@ -13,7 +13,7 @@ en_US fr_FR - 1.1.10 + 1.1.11 Chabreuil Antoine diff --git a/Controller/BrevoConfigController.php b/Controller/BrevoConfigController.php index f100278..b047534 100644 --- a/Controller/BrevoConfigController.php +++ b/Controller/BrevoConfigController.php @@ -52,6 +52,7 @@ public function saveAction(Request $request, ParserContext $parserContext, Brevo ConfigQuery::write(Brevo::CONFIG_NEWSLETTER_ID, $data['newsletter_list']); ConfigQuery::write(Brevo::CONFIG_THROW_EXCEPTION_ON_ERROR, (bool) $data['exception_on_errors']); ConfigQuery::write(Brevo::BREVO_ATTRIBUTES_MAPPING, $data['attributes_mapping']); + ConfigQuery::write(Brevo::BREVO_METADATA_MAPPING, $data['metadata_mapping']); $brevoApiService->enableEcommerce(); diff --git a/EventListeners/NewsletterListener.php b/EventListeners/NewsletterListener.php index 056f247..5656d1c 100644 --- a/EventListeners/NewsletterListener.php +++ b/EventListeners/NewsletterListener.php @@ -73,7 +73,7 @@ public function subscribe(NewsletterEvent $event) public function update(NewsletterEvent $event) { - if (null === BrevoNewsletterQuery::create()->findPk($event->getId()) || null !== NewsletterQuery::create()->findPk($event->getId())) { + if (null === BrevoNewsletterQuery::create()->findPk($event->getId()) && null === NewsletterQuery::create()->findPk($event->getId())) { return; } @@ -107,21 +107,14 @@ public function update(NewsletterEvent $event) public function unsubscribe(NewsletterEvent $event) { - if ((null === $model = BrevoNewsletterQuery::create()->findPk($event->getId())) || null !== NewsletterQuery::create()->findPk($event->getId())) { - return; - } - try { - $contact = $this->api->unsubscribe($event); - $status = $contact[1]; - if (null === $model) { - $model = BrevoNewsletterQuery::create()->findOneByEmail($event->getEmail()); - } + $contact = $this->api->unsubscribe($event->getEmail()); - if (null === $model) { + if (null === $model = BrevoNewsletterQuery::create()->findOneByEmail($event->getEmail())) { return; } + $status = $contact[1]; $data = ["id" => $model->getRelationId()]; $logMessage = $this->logAfterAction( sprintf("The email address '%s' was successfully unsubscribed from the list", $event->getEmail()), diff --git a/Form/BrevoConfigurationForm.php b/Form/BrevoConfigurationForm.php index 6a7c0f2..269a1f2 100644 --- a/Form/BrevoConfigurationForm.php +++ b/Form/BrevoConfigurationForm.php @@ -58,13 +58,19 @@ protected function buildForm(): void { $translator = Translator::getInstance(); - $defaultMapping = <<< END + $defaultCustomerMapping = <<< END { "customer_query": { - "EMAIL" : { - "select" : "customer.email" - } + "EMAIL" : { + "select" : "customer.email" } + } +} +END; + $defaultMetadataMapping = <<< END +{ + "product_query": { + } } END; $this->formBuilder @@ -131,9 +137,29 @@ protected function buildForm(): void 'required' => false, 'constraints' => [ new NotBlank(), - new Callback([$this, 'checkJsonValidity']), + new Callback([$this, 'checkCustomerJsonValidity']), ], - 'data' => ConfigQuery::read(Brevo::BREVO_ATTRIBUTES_MAPPING, $defaultMapping), + 'data' => ConfigQuery::read(Brevo::BREVO_ATTRIBUTES_MAPPING, $defaultCustomerMapping), + ]) + ->add('metadata_mapping', TextareaType::class, [ + 'label' => $translator->trans('Products metadata attributes mapping', [], Brevo::MESSAGE_DOMAIN), + 'attr' => [ + 'rows' => 10 + ], + 'label_attr' => [ + 'for' => 'attributes_mapping', + 'help' => Translator::getInstance()->trans( + 'This is a mapping of Brevo products meta-data attributes with Thelia products attributes. Do not change anything here if you do not know exactly what you are doing', + [], + Brevo::MESSAGE_DOMAIN + ) + ], + 'required' => false, + 'constraints' => [ + new NotBlank(), + new Callback([$this, 'checkProductJsonValidity']), + ], + 'data' => ConfigQuery::read(Brevo::BREVO_METADATA_MAPPING, $defaultMetadataMapping), ]) ->add('exception_on_errors', CheckboxType::class, [ 'label' => $translator->trans('Throw exception on Brevo error', [], Brevo::MESSAGE_DOMAIN), @@ -149,7 +175,17 @@ protected function buildForm(): void ]) ; } - public function checkJsonValidity($value, ExecutionContextInterface $context): void + + public function checkCustomerJsonValidity($value, ExecutionContextInterface $context): void + { + $this->checkJsonValidity('customer_query', $value, $context); + } + public function checkProductJsonValidity($value, ExecutionContextInterface $context): void + { + $this->checkJsonValidity('product_query', $value, $context); + } + + public function checkJsonValidity(string $expectedNode, $value, ExecutionContextInterface $context): void { if (empty($value)) { return; @@ -165,10 +201,10 @@ public function checkJsonValidity($value, ExecutionContextInterface $context): v ); } - if (! isset($jsonData['customer_query'])) { + if (! isset($jsonData[$expectedNode])) { $context->addViolation( Translator::getInstance()->trans( - "The customer attributes mapping JSON should contain a 'customer_query' field.", + "The customer attributes mapping JSON should contain a '$expectedNode' field.", [], Brevo::MESSAGE_DOMAIN ) diff --git a/Services/BrevoCustomerService.php b/Services/BrevoCustomerService.php index 3a55743..05e7b0f 100644 --- a/Services/BrevoCustomerService.php +++ b/Services/BrevoCustomerService.php @@ -25,10 +25,6 @@ public function __construct(private BrevoClient $brevoClient) public function createUpdateContact($customerId) { - if (empty(ConfigQuery::read(Brevo::BREVO_ATTRIBUTES_MAPPING, ''))) { - return null; - } - $customer = CustomerQuery::create()->findPk($customerId); try { @@ -40,7 +36,7 @@ public function createUpdateContact($customerId) throw $exception; } - return $this->brevoClient->createContact($customer); + return $this->brevoClient->createContact($customer->getEmail()); } } } diff --git a/Services/BrevoProductService.php b/Services/BrevoProductService.php index 2f97dcd..02613b6 100644 --- a/Services/BrevoProductService.php +++ b/Services/BrevoProductService.php @@ -12,10 +12,16 @@ namespace Brevo\Services; +use Brevo\Brevo; +use Brevo\Trait\DataExtractorTrait; +use Psr\EventDispatcher\EventDispatcherInterface; +use Thelia\Core\Event\Image\ImageEvent; +use Thelia\Core\Event\TheliaEvents; +use Thelia\Exception\TheliaProcessException; use Thelia\Log\Tlog; use Thelia\Model\Base\ProductQuery; use Thelia\Model\Cart; -use Thelia\Model\CategoryQuery; +use Thelia\Model\ConfigQuery; use Thelia\Model\Country; use Thelia\Model\Currency; use Thelia\Model\Order; @@ -27,8 +33,25 @@ class BrevoProductService { - public function __construct(private BrevoApiService $brevoApiService) + use DataExtractorTrait; + + protected string $baseSourceFilePath; + + protected array $metaDataMapping = []; + + public function __construct(private BrevoApiService $brevoApiService, protected EventDispatcherInterface $dispatcher) { + if (null === $this->baseSourceFilePath = ConfigQuery::read('images_library_path', null)) { + $this->baseSourceFilePath = THELIA_LOCAL_DIR.'media'.DS.'images'; + } else { + $this->baseSourceFilePath = THELIA_ROOT.$this->baseSourceFilePath; + } + + $mappingString = ConfigQuery::read(Brevo::BREVO_METADATA_MAPPING); + + if (!empty($mappingString) && null === $this->metaDataMapping = json_decode($mappingString, true)) { + throw new TheliaProcessException('Product metadata mapping error: JSON data seems invalid, please check syntax.'); + } } public function getObjName() @@ -80,22 +103,61 @@ public function exportInBatch($limit, $offset, $locale, Currency $currency, Coun public function getData(Product $product, $locale, Currency $currency, Country $country) { $product->setLocale($locale); - $imagePath = ProductImageQuery::create()->filterByProductId($product->getId())->orderByPosition()->findOne()?->getFile(); $productPrice = $product->getDefaultSaleElements()->getPricesByCurrency($currency); + $productSku = $product->getDefaultSaleElements()->getEanCode(); $categories = $product->getCategories(); $categoryIds = array_map(function ($category) { return (string) $category['Id']; }, $categories->toArray()); + // Get first product image + $imageUrl = null; + + if (null !== $productImage = ProductImageQuery::create() + ->filterByProductId($product->getId()) + ->filterByVisible(1) + ->orderBy('position')->findOne() + ) { + // Put source image file path + $sourceFilePath = sprintf( + '%s/%s/%s', + $this->baseSourceFilePath, + 'product', + $productImage->getFile() + ); + + // Create image processing event + $event = (new ImageEvent()) + ->setSourceFilepath($sourceFilePath) + ->setCacheSubdirectory('product') + ; + + try { + // Dispatch image processing event + $this->dispatcher->dispatch($event, TheliaEvents::IMAGE_PROCESS); + $imageUrl = $event->getFileUrl(); + } catch (\Exception $ex) { + // Ignore the result + $a = 1; + } + } + return [ - 'categories' => $categoryIds, - 'id' => (string) $product->getId(), - 'name' => $product->getTitle(), - 'url' => URL::getInstance()?->absoluteUrl($product->getUrl()), - 'image' => $imagePath ? URL::getInstance()?->absoluteUrl('/cache/images/product/'.$imagePath) : null, - 'sku' => $product->getRef(), - 'price' => round((float) $product->getTaxedPrice($country, $productPrice->getPrice()), 2), + 'categories' => $categoryIds, + 'id' => (string) $product->getId(), + 'name' => $product->getTitle(), + 'url' => $product->getUrl($locale), + 'image' => $imageUrl, + 'sku' => $productSku ?? $product->getRef(), + 'price' => round((float) $product->getTaxedPrice($country, $productPrice->getPrice()), 2), + 'metaInfo' => $this->getMappedValues( + $this->metaDataMapping, + 'product_query', + 'product', + 'product.id', + $product->getId(), + ), ]; } diff --git a/Trait/DataExtractorTrait.php b/Trait/DataExtractorTrait.php new file mode 100644 index 0000000..7b53003 --- /dev/null +++ b/Trait/DataExtractorTrait.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/* web : https://www.openstudio.fr */ + +/* For the full copyright and license information, please view the LICENSE */ +/* file that was distributed with this source code. */ + +/** + * Created by Franck Allimant, OpenStudio + * Projet: thelia25 + * Date: 17/11/2023. + */ + +namespace Brevo\Trait; + +use Propel\Runtime\Connection\ConnectionWrapper; +use Propel\Runtime\Propel; +use Thelia\Exception\TheliaProcessException; +use Thelia\Log\Tlog; + +trait DataExtractorTrait +{ + public function getMappedValues( + array $jsonMapping, + string $mapKey, + string $sourceTableName, + string $selectorFieldName, + mixed $selector, + int $selectorType = \PDO::PARAM_INT + ): array { + try { + if (empty($jsonMapping)) { + return []; + } + + if (!\array_key_exists($mapKey, $jsonMapping)) { + throw new TheliaProcessException("Mapping error : '$mapKey' element is missing in JSON data"); + } + + $attributes = []; + + /** @var ConnectionWrapper $con */ + $con = Propel::getConnection(); + + foreach ($jsonMapping[$mapKey] as $key => $dataQuery) { + if (!\array_key_exists('select', $dataQuery)) { + throw new \Exception("Mapping error : 'select' element missing in ".$key.' query'); + } + + try { + $sql = 'SELECT '.$dataQuery['select'].' AS '.$key.' FROM '.$sourceTableName; + + if (\array_key_exists('join', $dataQuery)) { + if (!\is_array($dataQuery['join'])) { + $dataQuery['join'] = [$dataQuery['join']]; + } + + foreach ($dataQuery['join'] as $join) { + $sql .= ' LEFT JOIN '.$join; + } + } + + $sql .= ' WHERE '.$selectorFieldName.' = :selector'; + + if (\array_key_exists('groupBy', $dataQuery)) { + $sql .= ' GROUP BY '.$dataQuery['groupBy']; + } + + $stmt = $con->prepare($sql); + $stmt->bindValue(':selector', $selector, $selectorType); + $stmt->execute(); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $attributes[$key] = $row[$key]; + if (\array_key_exists($key, $jsonMapping) && \array_key_exists($row[$key], $jsonMapping[$key])) { + $attributes[$key] = $jsonMapping[$key][$row[$key]]; + } + } + } catch (\Exception $ex) { + Tlog::getInstance()->error( + 'Failed to execute SQL request to map Brevo attribute. Error is '.$ex->getMessage().", request is : $sql"); + } + } + + return $attributes; + } catch (\Exception $ex) { + throw new TheliaProcessException( + 'Mapping error : configuration is missing or invalid, please go to the module configuration and define the JSON mapping to match thelia attribute with brevo attribute. Error is : '.$ex->getMessage() + ); + } + } +} diff --git a/templates/backOffice/default/brevo-configuration.html b/templates/backOffice/default/brevo-configuration.html index bd25077..4fc74ac 100644 --- a/templates/backOffice/default/brevo-configuration.html +++ b/templates/backOffice/default/brevo-configuration.html @@ -24,8 +24,9 @@ {render_form_field field="api_key"} {render_form_field field="automation_key"} {render_form_field field="newsletter_list"} - {render_form_field field="attributes_mapping" extra_class="fixedfont"} {render_form_field field="exception_on_errors"} + {render_form_field field="attributes_mapping" extra_class="fixedfont"} + {render_form_field field="metadata_mapping" extra_class="fixedfont"} {include "includes/inner-form-toolbar.html" hide_flags = 1 close_url={url path='/admin/modules'} page_bottom=1}