src/UniSport/Bundle/AppBundle/EventListener/KlaviyoSubscriber.php line 142

Open in your IDE?
  1. <?php
  2. declare(strict_types=0);
  3. /*
  4.  * WellCommerce Foundation
  5.  *
  6.  * This file is part of the WellCommerce package.
  7.  *
  8.  * (c) Adam Piotrowski <adam@wellcommerce.org>, Adrian Potepa <adrian@wellcommerce.org>
  9.  *
  10.  * For the full copyright and license information,
  11.  * please view the LICENSE file that was distributed with this source code.
  12.  */
  13. namespace UniSport\Bundle\AppBundle\EventListener;
  14. use KlaviyoAPI\API\EventsApi;
  15. use KlaviyoAPI\API\ListsApi;
  16. use KlaviyoAPI\API\ProfilesApi;
  17. use KlaviyoAPI\KlaviyoAPI;
  18. use KlaviyoAPI\Model\EventCreateQueryV2;
  19. use KlaviyoAPI\Model\ListMembersAddQuery;
  20. use KlaviyoAPI\Model\ProfileUpsertQuery;
  21. use Psr\Container\ContainerInterface;
  22. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  23. use WellCommerce\Bundle\AppBundle\Entity\AddressInterface;
  24. use WellCommerce\Bundle\AppBundle\Entity\ClientBillingAddress;
  25. use WellCommerce\Bundle\AppBundle\Entity\ClientContactDetails;
  26. use WellCommerce\Bundle\AppBundle\Entity\Media;
  27. use WellCommerce\Bundle\AppBundle\Entity\NewsletterSubscriber;
  28. use WellCommerce\Bundle\AppBundle\Entity\Shop;
  29. use WellCommerce\Bundle\CatalogBundle\Entity\Producer;
  30. use WellCommerce\Bundle\CoreBundle\DependencyInjection\AbstractServiceSubscriber;
  31. use WellCommerce\Bundle\CoreBundle\Doctrine\Event\EntityEvent;
  32. use WellCommerce\Bundle\OrderBundle\DataSet\Admin\OrderStatusDataSet;
  33. use WellCommerce\Bundle\OrderBundle\Entity\Order;
  34. use WellCommerce\Bundle\OrderBundle\Entity\OrderProduct;
  35. use WellCommerce\Bundle\OrderBundle\Entity\OrderStatus;
  36. use WellCommerce\Bundle\OrderBundle\Entity\OrderStatusHistory;
  37. use WellCommerce\Component\Form\Event\FormEvent;
  38. /**
  39.  * Class KlaviyoSubscriber
  40.  *
  41.  * @author  Adam Piotrowski <adam@wellcommerce.org>
  42.  */
  43. class KlaviyoSubscriber extends AbstractServiceSubscriber implements EventSubscriberInterface
  44. {
  45.     protected OrderStatusDataSet $dataSet;
  46.     protected ?KlaviyoAPI        $api null;
  47.     public function __construct(ContainerInterface $locatorOrderStatusDataSet $dataSet)
  48.     {
  49.         parent::__construct($locator);
  50.         $this->dataSet $dataSet;
  51.     }
  52.     public static function getSubscribedEvents(): array
  53.     {
  54.         return [
  55.             'order.post_confirm'                => 'onOrderPostConfirm',
  56.             'admin.shop.pre_form_init'          => ['onShopFormAdminInit'],
  57.             'order_status_history.post_create'  => ['onOrderStatusHistoryPostCreate'0],
  58.             'newsletter_subscriber.post_create' => ['onNewsletterSubscriberPostCreate'0],
  59.         ];
  60.     }
  61.     public function onShopFormAdminInit(FormEvent $event)
  62.     {
  63.         $resource $event->getResource();
  64.         if ($resource instanceof Shop) {
  65.             $form    $event->getForm();
  66.             $builder $event->getFormBuilder();
  67.             $klaviyoData $form->addChild(
  68.                 $builder->getElement('nested_fieldset', [
  69.                     'name'  => 'klaviyo_data',
  70.                     'label' => 'klaviyo.fieldset.settings',
  71.                 ])
  72.             );
  73.             $klaviyoData->addChild(
  74.                 $builder->getElement('text_field', [
  75.                     'name'  => 'klaviyoApiPublicKey',
  76.                     'label' => 'klaviyo.label.public_key',
  77.                 ])
  78.             );
  79.             $klaviyoData->addChild(
  80.                 $builder->getElement('password', [
  81.                     'name'  => 'klaviyoApiPrivateKey',
  82.                     'label' => 'klaviyo.label.private_key',
  83.                 ])
  84.             );
  85.             $klaviyoData->addChild(
  86.                 $builder->getElement('text_field', [
  87.                     'name'  => 'klaviyoMainListId',
  88.                     'label' => 'klaviyo.label.main_list_id',
  89.                 ])
  90.             );
  91.             $klaviyoData->addChild(
  92.                 $builder->getElement('checkbox', [
  93.                     'name'  => 'klaviyoApiEnabled',
  94.                     'label' => 'klaviyo.label.enabled',
  95.                 ])
  96.             );
  97.             $orderStatuses $this->dataSet->getResult('select', [], ['default_option' => '---']);
  98.             $klaviyoData->addChild(
  99.                 $builder->getElement('select', [
  100.                     'name'        => 'klaviyoStatusOrderFulfilled',
  101.                     'label'       => 'klaviyo.label.order_status_fulfilled',
  102.                     'options'     => $orderStatuses,
  103.                     'transformer' => $builder->getRepositoryTransformer('entity'OrderStatus::class),
  104.                 ])
  105.             );
  106.             $klaviyoData->addChild(
  107.                 $builder->getElement('select', [
  108.                     'name'        => 'klaviyoStatusOrderCancelled',
  109.                     'label'       => 'klaviyo.label.order_status_cancelled',
  110.                     'options'     => $orderStatuses,
  111.                     'transformer' => $builder->getRepositoryTransformer('entity'OrderStatus::class),
  112.                 ])
  113.             );
  114.             $klaviyoData->addChild(
  115.                 $builder->getElement('select', [
  116.                     'name'        => 'klaviyoStatusOrderRefunded',
  117.                     'label'       => 'klaviyo.label.order_status_refunded',
  118.                     'options'     => $orderStatuses,
  119.                     'transformer' => $builder->getRepositoryTransformer('entity'OrderStatus::class),
  120.                 ])
  121.             );
  122.         }
  123.     }
  124.     public function onOrderPostConfirm(EntityEvent $event)
  125.     {
  126.         $order $event->getEntity();
  127.         if ($order instanceof Order) {
  128.             $shop $order->getShop();
  129.             if ($shop->isKlaviyoApiEnabled()) {
  130.                 $this->sendStatusPlaced($shop$order);
  131.             }
  132.         }
  133.     }
  134.     public function onNewsletterSubscriberPostCreate(EntityEvent $event)
  135.     {
  136.         /** @var NewsletterSubscriber $newsletterSubscriber */
  137.         $newsletterSubscriber $event->getEntity();
  138.         if ($newsletterSubscriber instanceof NewsletterSubscriber) {
  139.             $shop   $newsletterSubscriber->getShop();
  140.             $listId $shop->getKlaviyoMainListId();
  141.             if ($shop->isKlaviyoApiEnabled() && !empty($listId)) {
  142.                 $this->sendListSubscribe($shop$listId$newsletterSubscriber->getEmail());
  143.             }
  144.         }
  145.     }
  146.     public function onOrderStatusHistoryPostCreate(EntityEvent $event)
  147.     {
  148.         $history $event->getEntity();
  149.         if ($history instanceof OrderStatusHistory) {
  150.             $order  $history->getOrder();
  151.             $shop   $order->getShop();
  152.             $status $history->getOrderStatus();
  153.             if ($shop->isKlaviyoApiEnabled()) {
  154.                 $statusFulfilled $shop->getKlaviyoStatusOrderFulfilled();
  155.                 if ($statusFulfilled instanceof OrderStatus) {
  156.                     if ($status->getId() === $statusFulfilled->getId()) {
  157.                         $this->sendOrderStatus($shop$order'Fulfilled''Fulfilled Order');
  158.                     }
  159.                 }
  160.                 $statusCancelled $shop->getKlaviyoStatusOrderCancelled();
  161.                 if ($statusCancelled instanceof OrderStatus) {
  162.                     if ($status->getId() === $statusCancelled->getId()) {
  163.                         $this->sendOrderStatus($shop$order'Cancelled''Cancelled Order');
  164.                     }
  165.                 }
  166.                 $statusRefunded $shop->getKlaviyoStatusOrderRefunded();
  167.                 if ($statusRefunded instanceof OrderStatus) {
  168.                     if ($status->getId() === $statusRefunded->getId()) {
  169.                         $this->sendOrderStatus($shop$order'Refunded''Refunded Order');
  170.                     }
  171.                 }
  172.             }
  173.         }
  174.     }
  175.     private function getApiClient(Shop $shop): KlaviyoAPI
  176.     {
  177.         if (!$this->api instanceof KlaviyoAPI) {
  178.             $this->api = new KlaviyoAPI($shop->getKlaviyoApiPrivateKey());
  179.         }
  180.         return $this->api;
  181.     }
  182.     private function sendListSubscribe(Shop $shop$listId$email)
  183.     {
  184.         /** @var ListsApi $api */
  185.         $api     $this->getApiClient($shop)->Lists;
  186.         $profile $this->getProfile($shop$email);
  187.         if (null === $profile) {
  188.             return;
  189.         }
  190.         $data   = [];
  191.         $data[] = [
  192.             'type' => 'profile',
  193.             'id'   => $profile,
  194.         ];
  195.         $query = new ListMembersAddQuery(['data' => $data]);
  196.         try {
  197.             $api->createListRelationships($listId$query);
  198.         } catch (\Throwable $ex) {
  199.             if ($this->getKernel()->isDebug()) {
  200.                 echo $ex->getMessage();
  201.                 die();
  202.             }
  203.         }
  204.     }
  205.     private function getProfile(Shop $shopstring $email): ?string
  206.     {
  207.         /** @var ProfilesApi $api */
  208.         $api $this->getApiClient($shop)->Profiles;
  209.         $query = new ProfileUpsertQuery([
  210.             'data' => [
  211.                 'type'       => 'profile',
  212.                 'attributes' => [
  213.                     'email' => $email,
  214.                 ],
  215.             ],
  216.         ]);
  217.         try {
  218.             $response $api->createOrUpdateProfile($query);
  219.             return $response['data']['id'] ?? null;
  220.         } catch (\Throwable $throwable) {
  221.             return null;
  222.         }
  223.     }
  224.     private function sendStatusPlaced(Shop $shopOrder $order)
  225.     {
  226.         $this->sendOrderStatus($shop$order'Placed''Placed Order');
  227.         foreach ($order->getProducts() as $orderProduct) {
  228.             $this->sendOrderedProduct($shop$order$orderProduct);
  229.         }
  230.     }
  231.     private function sendOrderedProduct(Shop $shopOrder $orderOrderProduct $orderProduct)
  232.     {
  233.         /** @var EventsApi $api */
  234.         $api                     $this->getApiClient($shop)->Events;
  235.         $contactDetails          $order->getContactDetails();
  236.         $billingAddress          $order->getBillingAddress();
  237.         $properties              $this->prepareProduct($orderProduct);
  238.         $product                 $orderProduct->getProduct();
  239.         $properties['Reason']    = 'Ordered Product';
  240.         $properties['$event_id'] = $order->getId() . '_' $product->getSku();
  241.         $properties['$value']    = $orderProduct->getSellPrice()->getGrossAmount();
  242.         $data = [
  243.             'type'       => 'event',
  244.             'attributes' => [
  245.                 'properties'     => $properties,
  246.                 'value'          => $order->getSummary()->getGrossAmount(),
  247.                 'value_currency' => $order->getCurrency(),
  248.                 'metric'         => [
  249.                     'data' => [
  250.                         'type'       => 'metric',
  251.                         'attributes' => [
  252.                             'name' => 'Ordered Product',
  253.                         ],
  254.                     ],
  255.                 ],
  256.                 'profile'        => $this->prepareProfile($billingAddress$contactDetails),
  257.             ],
  258.         ];
  259.         $params['data'] = $data;
  260.         $query          = new EventCreateQueryV2($params);
  261.         try {
  262.             $api->createEvent($query);
  263.         } catch (\Throwable $throwable) {
  264.             if ($this->getKernel()->isDebug()) {
  265.                 echo $throwable->getMessage();
  266.                 die();
  267.             }
  268.         }
  269.     }
  270.     private function sendOrderStatus(Shop $shopOrder $orderstring $reasonstring $metric)
  271.     {
  272.         /** @var EventsApi $api */
  273.         $api                  $this->getApiClient($shop)->Events;
  274.         $contactDetails       $order->getContactDetails();
  275.         $billingAddress       $order->getBillingAddress();
  276.         $properties           $this->prepareOrderProperties($order);
  277.         $properties['Reason'] = $reason;
  278.         $data = [
  279.             'type'       => 'event',
  280.             'attributes' => [
  281.                 'properties'     => $properties,
  282.                 'value'          => $order->getSummary()->getGrossAmount(),
  283.                 'value_currency' => $order->getCurrency(),
  284.                 'metric'         => [
  285.                     'data' => [
  286.                         'type'       => 'metric',
  287.                         'attributes' => [
  288.                             'name' => $metric,
  289.                         ],
  290.                     ],
  291.                 ],
  292.                 'profile'        => $this->prepareProfile($billingAddress$contactDetails),
  293.             ],
  294.         ];
  295.         $params['data'] = $data;
  296.         $query          = new EventCreateQueryV2($params);
  297.         try {
  298.             $api->createEvent($query);
  299.         } catch (\Throwable $throwable) {
  300.             if ($this->getKernel()->isDebug()) {
  301.                 echo $throwable->getMessage();
  302.                 die();
  303.             }
  304.         }
  305.     }
  306.     private function prepareItems(Order $order): array
  307.     {
  308.         $items = [];
  309.         foreach ($order->getProducts() as $orderProduct) {
  310.             $items[] = $this->prepareProduct($orderProduct);
  311.         }
  312.         return $items;
  313.     }
  314.     private function prepareProduct(OrderProduct $orderProduct): array
  315.     {
  316.         $product    $orderProduct->getProduct();
  317.         $categories = [];
  318.         foreach ($product->getCategories() as $category) {
  319.             $categories[] = $category->translate()->getName();
  320.         }
  321.         $image '';
  322.         if ($product->getPhoto() instanceof Media) {
  323.             $image $this->getImageHelper()->getImage($product->getPhoto()->getPath(), 'original');
  324.         }
  325.         return [
  326.             'ProductID'   => $product->getId(),
  327.             'SKU'         => $product->getSku(),
  328.             'ProductName' => $product->translate()->getName(),
  329.             'Quantity'    => $orderProduct->getQuantity(),
  330.             'ItemPrice'   => $orderProduct->getSellPrice()->getGrossAmount(),
  331.             'RowTotal'    => round($orderProduct->getSellPrice()->getGrossAmount() * $orderProduct->getQuantity(), 2),
  332.             'ProductURL'  => $this->getRouterHelper()->generateUrl($product->translate()->getRoutePath()),
  333.             'ImageURL'    => $image,
  334.             'Categories'  => $categories,
  335.             'Brand'       => $product->getProducer() instanceof Producer $product->getProducer()->translate()->getName() : '',
  336.         ];
  337.     }
  338.     private function getOrderCategories(Order $order): array
  339.     {
  340.         $items = [];
  341.         foreach ($order->getProducts() as $orderProduct) {
  342.             $product $orderProduct->getProduct();
  343.             foreach ($product->getCategories() as $category) {
  344.                 $items[] = $category->translate()->getName();
  345.             }
  346.         }
  347.         return array_unique($items);
  348.     }
  349.     private function getOrderItemNames(Order $order): array
  350.     {
  351.         $items = [];
  352.         foreach ($order->getProducts() as $orderProduct) {
  353.             $product $orderProduct->getProduct();
  354.             $items[] = $product->translate()->getName();
  355.         }
  356.         return $items;
  357.     }
  358.     private function getOrderBrands(Order $order): array
  359.     {
  360.         $items = [];
  361.         foreach ($order->getProducts() as $orderProduct) {
  362.             $product  $orderProduct->getProduct();
  363.             $producer $product->getProducer();
  364.             if ($producer instanceof Producer) {
  365.                 $items[] = $producer->translate()->getName();
  366.             }
  367.         }
  368.         return array_unique($items);
  369.     }
  370.     private function prepareAddress(AddressInterface $addressClientContactDetails $contactDetails): array
  371.     {
  372.         return [
  373.             'FirstName'   => $address->getFirstName(),
  374.             'LastName'    => $address->getLastName(),
  375.             'Company'     => $address->getCompanyName(),
  376.             'Address1'    => $address->getLine1(),
  377.             'Address2'    => trim($address->getLine2() . ' ' $address->getLine3()),
  378.             'City'        => $address->getCity(),
  379.             'Region'      => $address->getState(),
  380.             'RegionCode'  => $address->getState(),
  381.             'Country'     => $address->getCountry(),
  382.             'CountryCode' => $address->getCountry(),
  383.             'Zip'         => $address->getPostalCode(),
  384.             'Phone'       => $contactDetails->getPhone(),
  385.         ];
  386.     }
  387.     private function prepareProfile(ClientBillingAddress $billingAddressClientContactDetails $contactDetails): array
  388.     {
  389.         return [
  390.             'data' => [
  391.                 'type'       => 'profile',
  392.                 'attributes' => [
  393.                     'phone_number' => '+48' $contactDetails->getPhone(),
  394.                     'email'        => $contactDetails->getEmail(),
  395.                     'first_name'   => $contactDetails->getFirstName(),
  396.                     'last_name'    => $contactDetails->getLastName(),
  397.                     'organization' => $billingAddress->getCompanyName(),
  398.                     'location'     => [
  399.                         'address1' => $billingAddress->getLine1(),
  400.                         'address2' => trim($billingAddress->getLine2() . ' ' $billingAddress->getLine3()),
  401.                         'city'     => $billingAddress->getCity(),
  402.                         'country'  => $billingAddress->getCountry(),
  403.                         'region'   => $billingAddress->getState(),
  404.                         'zip'      => $billingAddress->getPostalCode(),
  405.                     ],
  406.                     'properties'   => [
  407.                         '$email'        => $contactDetails->getEmail(),
  408.                         '$first_name'   => $contactDetails->getFirstName(),
  409.                         '$last_name'    => $contactDetails->getLastName(),
  410.                         '$phone_number' => $contactDetails->getPhone(),
  411.                         '$address1'     => $billingAddress->getLine1(),
  412.                         '$address2'     => trim($billingAddress->getLine2() . ' ' $billingAddress->getLine3()),
  413.                         '$city'         => $billingAddress->getCity(),
  414.                         '$zip'          => $billingAddress->getPostalCode(),
  415.                         '$region'       => $billingAddress->getState(),
  416.                         '$country'      => $billingAddress->getCountry(),
  417.                     ],
  418.                 ],
  419.             ],
  420.         ];
  421.     }
  422.     private function prepareOrderProperties(Order $order): array
  423.     {
  424.         $contactDetails  $order->getContactDetails();
  425.         $billingAddress  $order->getBillingAddress();
  426.         $shippingAddress $order->getShippingAddress();
  427.         $discountCode    '';
  428.         $discountValue   0;
  429.         if ($order->hasModifier('coupon_discount')) {
  430.             $discountValue $order->getModifier('coupon_discount')->getGrossAmount();
  431.             $discountCode  $order->getCoupon()->getCode();
  432.         }
  433.         return [
  434.             '$event_id'       => $order->getId(),
  435.             '$value'          => $order->getSummary()->getGrossAmount(),
  436.             'OrderId'         => $order->getId(),
  437.             'Categories'      => $this->getOrderCategories($order),
  438.             'ItemNames'       => $this->getOrderItemNames($order),
  439.             'Brands'          => $this->getOrderBrands($order),
  440.             'DiscountCode'    => $discountCode,
  441.             'DiscountValue'   => $discountValue,
  442.             'Items'           => $this->prepareItems($order),
  443.             'BillingAddress'  => $this->prepareAddress($billingAddress$contactDetails),
  444.             'ShippingAddress' => $this->prepareAddress($shippingAddress$contactDetails),
  445.         ];
  446.     }
  447. }